Compare commits
174 Commits
main
...
30109fdb79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30109fdb79 | ||
|
|
83226f006c | ||
|
|
a5e21c93b3 | ||
|
|
1989c22562 | ||
|
|
882740592c | ||
|
|
0c995054a2 | ||
|
|
6780c0fbb1 | ||
|
|
7101daeb90 | ||
|
|
0d0de45a25 | ||
|
|
28b6153ab0 | ||
|
|
7a4fc0736d | ||
|
|
9912f310ec | ||
|
|
7bf1a0bd57 | ||
|
|
810dd2351b | ||
|
|
e42975159f | ||
|
|
c1cff1d61b | ||
|
|
dbe4557dc3 | ||
|
|
bc7099cce2 | ||
|
|
d75e956fbf | ||
|
|
6eda04a81e | ||
|
|
069b86de13 | ||
|
|
833d43d7d1 | ||
| c9b67c4d3b | |||
| a8510445cd | |||
|
|
e1ca896764 | ||
|
|
7a6bd28de5 | ||
|
|
85a158ea3e | ||
|
|
7fc0e3bace | ||
|
|
7af8bc96c8 | ||
|
|
64ac0c7e16 | ||
|
|
7b071bc585 | ||
|
|
3058adfdb7 | ||
|
|
88bd58fc66 | ||
|
|
274ea15dcc | ||
|
|
d863376b41 | ||
|
|
5bbc71654a | ||
|
|
9d41602320 | ||
|
|
4e0faed88e | ||
| 3fa7d407d2 | |||
| 1fa60557df | |||
| 2b273ec70a | |||
| 45af83d0b2 | |||
| b4ea8907d7 | |||
| 7ed87f59ee | |||
|
|
567ae02c48 | ||
|
|
ae6f14efa9 | ||
| 048a548df8 | |||
| b9112a5606 | |||
|
|
8f1fea30ee | ||
|
|
29704f9b36 | ||
| 6cd54cda18 | |||
| 38c0b88abf | |||
|
|
e40b707501 | ||
|
|
6cb1e72798 | ||
|
|
11876f7fff | ||
|
|
59541a9d3d | ||
|
|
466d278b29 | ||
| 6fa5ade5b1 | |||
| c6b1efe719 | |||
| fa3063b3b5 | |||
| ebd5ceac41 | |||
| 9c9b219f3b | |||
|
|
73aca07391 | ||
|
|
780270882e | ||
|
|
f8e4ab8cdb | ||
|
|
bb53b6e486 | ||
|
|
e09c01cb7d | ||
|
|
2903553088 | ||
|
|
8c5105052d | ||
|
|
0e1e2cec39 | ||
|
|
086668d31b | ||
| d4da1b47ef | |||
| 1596b46ff1 | |||
| fd0ec4f7ff | |||
|
|
012c4036e0 | ||
| 688fb3daa0 | |||
|
|
5e5059ea73 | ||
|
|
386a103df1 | ||
| aad884d07c | |||
| f011300bef | |||
|
|
15f5c6b3a2 | ||
|
|
7fbd721512 | ||
| 271b8af4c4 | |||
| 81e230b79f | |||
|
|
7da3dcb0d7 | ||
| c6f3a44b81 | |||
| 0e16681404 | |||
|
|
b8f53e9f4a | ||
|
|
697dc36df4 | ||
|
|
5672307e33 | ||
|
|
fd80e2d3c7 | ||
| b160709f16 | |||
|
|
b52c96fa67 | ||
| 5a7e5e92a8 | |||
| 61dd9fb1c5 | |||
|
|
3d202e32c2 | ||
| 07b2334d61 | |||
| 892d96b904 | |||
| 41a42b1133 | |||
|
|
467ac9c24f | ||
|
|
1d4478e98e | ||
|
|
e24318e8ee | ||
|
|
5ad2e40221 | ||
|
|
c82afcbfd6 | ||
|
|
95d85572f3 | ||
|
|
727636e0f8 | ||
|
|
b01a375acc | ||
|
|
b3c396ba9c | ||
|
|
260db8e896 | ||
|
|
b5f393ceb7 | ||
| b1bea281ec | |||
| c9b65b6090 | |||
| 0ac6d6e93f | |||
| 743b3f0ef6 | |||
| 17f0045dbe | |||
|
|
1ae365b1f3 | ||
| b16c5c3263 | |||
| 8ed58d37d8 | |||
|
|
84175e94d1 | ||
|
|
ed83044f81 | ||
| 9cb6be3098 | |||
| 4ab4578081 | |||
|
|
652d89d3be | ||
|
|
17edeef461 | ||
|
|
aad6919ec3 | ||
|
|
baf161e695 | ||
|
|
51751f6b5e | ||
|
|
c1b051a185 | ||
| 1a5e285f09 | |||
| 911d1d8477 | |||
| e0261d4a37 | |||
| 50cb33ac43 | |||
| dcb63f88ae | |||
|
|
8dd9ddc93e | ||
|
|
2dc6bd1346 | ||
|
|
703d9cf781 | ||
| 261064bd23 | |||
| a1e8f3295e | |||
| a89c199ea8 | |||
| 5dc7514f05 | |||
| 925541ab99 | |||
| b800ca6b74 | |||
| 4a4afc4b10 | |||
| 26a55cea1d | |||
| 5e68456707 | |||
| e992aa0ecd | |||
|
|
69c32905e1 | ||
|
|
c582de3f60 | ||
|
|
0a8074eef8 | ||
| 4b90bd5928 | |||
| 4376c8c313 | |||
| 4746ff22a1 | |||
| 68f8a413bf | |||
| c10d05ead2 | |||
| de641d18d7 | |||
|
|
8a0beee181 | ||
|
|
3dcb6330e3 | ||
|
|
4bd8a54b34 | ||
|
|
8368c9382a | ||
| f2463da8cc | |||
| 6b8027f449 | |||
|
|
8c3fea8a24 | ||
|
|
819093db8c | ||
|
|
7dcfc3e705 | ||
|
|
7bb8b227b4 | ||
|
|
3d2fddbe7b | ||
|
|
9662610b1b | ||
|
|
56f958173b | ||
|
|
0e57e4de46 | ||
|
|
b0e365dcde | ||
|
|
5497f4fdbc | ||
|
|
3d6b622eef | ||
|
|
38ac7da504 | ||
|
|
1c895710d8 |
@@ -1,56 +0,0 @@
|
||||
name: AiDA WEB-Node.js Develop 分支构建部署123
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- dev_vite
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: "contains(github.event.head_commit.message, '[run build]')"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 18.18.0 ]
|
||||
|
||||
env:
|
||||
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/DevelopVersion/develop-aida-web-front
|
||||
|
||||
steps:
|
||||
- name: 1.检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 2.设置 Node.js 环境
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run build:dev
|
||||
- run: ls -l
|
||||
|
||||
- name: 3.同步文件到远程服务器
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
source: "./dist/*"
|
||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
ssh_options: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
strip_components: 0
|
||||
|
||||
- name: 4. 远程重载 Nginx 配置
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
# 核心:执行 Nginx 重载命令
|
||||
script: |
|
||||
echo "尝试重载 Nginx 服务..."
|
||||
# 💡 注意:执行此命令需要服务器用户具有 sudo 权限,并且配置了 NOPASSWD。
|
||||
# 否则工作流可能会因为权限不足而失败。
|
||||
sudo systemctl reload nginx
|
||||
echo "Nginx 重载命令已发送。"
|
||||
@@ -1,50 +0,0 @@
|
||||
name: 手动触发 AiDA WEB-Node.js Develop 分支构建部署
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 18.18.0 ]
|
||||
env:
|
||||
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/DevelopVersion/develop-aida-web-front
|
||||
|
||||
steps:
|
||||
- name: 1.检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 2.设置 Node.js 环境
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run build:dev
|
||||
- run: ls -l
|
||||
|
||||
- name: 3.同步文件到远程服务器
|
||||
uses: appleboy/scp-action@v0.1.7
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
source: "./dist/*"
|
||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
ssh_options: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
strip_components: 0
|
||||
|
||||
- name: 4. 远程重载 Nginx 配置
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
# 核心:执行 Nginx 重载命令
|
||||
script: |
|
||||
echo "尝试重载 Nginx 服务..."
|
||||
# 💡 注意:执行此命令需要服务器用户具有 sudo 权限,并且配置了 NOPASSWD。
|
||||
# 否则工作流可能会因为权限不足而失败。
|
||||
sudo systemctl reload nginx
|
||||
echo "Nginx 重载命令已发送。"
|
||||
@@ -1,56 +0,0 @@
|
||||
name: AiDA WEB-Node.js StableVersion 分支构建部署
|
||||
on:
|
||||
schedule:
|
||||
# cron为UTC时区,构建时间=部署时间-8小时 {*分 (-8)时 *日 *月 *周} ---
|
||||
# 示例: 1月1日22点22分触发构建 cron写作 - '22 14 1 1 *'
|
||||
- cron: '22 14 1 1 *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 18.18.0 ]
|
||||
|
||||
steps:
|
||||
- name: 1.检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: StableVersion
|
||||
|
||||
- name: 2.打印当前分支信息
|
||||
run: |
|
||||
echo "Current branch being deployed is: $(git rev-parse --abbrev-ref HEAD)"
|
||||
echo "The code is from the 'main' branch, as specified in 'actions/checkout'."
|
||||
|
||||
- name: 3.设置 Node.js 环境 ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- run: ls -l
|
||||
|
||||
- name: 3.5. 手动安装 AWS CLI v2 # 新增步骤:确保 aws 命令可用
|
||||
run: |
|
||||
echo "安装 AWS CLI V2..."
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
unzip awscliv2.zip
|
||||
sudo ./aws/install --update
|
||||
aws --version
|
||||
echo "AWS CLI V2 安装完成。"
|
||||
|
||||
- name: 4.配置 AWS 凭证
|
||||
uses: aws-actions/configure-aws-credentials@main
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: 'ap-east-1'
|
||||
|
||||
- name: 5.同步 dist 目录到 S3
|
||||
run: |
|
||||
aws s3 sync dist/* s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
||||
|
||||
- name: 6.部署完成
|
||||
run: echo "构建和部署到 S3 任务完成。"
|
||||
1
.gitignore
vendored
@@ -24,3 +24,4 @@ dist.rar
|
||||
*.sw?
|
||||
.eslintrc-auto-import.json
|
||||
components.d.ts
|
||||
.cursor
|
||||
260
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^9.6.1",
|
||||
"vue-router": "^4.0.3",
|
||||
"vue3-moveable": "^0.28.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0",
|
||||
"x-sender": "^1.1.6"
|
||||
@@ -232,6 +233,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfcs/core": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@cfcs/core/-/core-0.0.6.tgz",
|
||||
"integrity": "sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@egjs/component": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@ctrl/tinycolor": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||
@@ -240,6 +250,39 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@daybrush/utils": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/@daybrush/utils/-/utils-1.13.0.tgz",
|
||||
"integrity": "sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@egjs/agent": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/@egjs/agent/-/agent-2.4.4.tgz",
|
||||
"integrity": "sha512-cvAPSlUILhBBOakn2krdPnOGv5hAZq92f1YHxYcfu0p7uarix2C6Ia3AVizpS1SGRZGiEkIS5E+IVTLg1I2Iog==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@egjs/children-differ": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@egjs/children-differ/-/children-differ-1.0.1.tgz",
|
||||
"integrity": "sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@egjs/list-differ": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/component": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@egjs/component/-/component-3.0.5.tgz",
|
||||
"integrity": "sha512-cLcGizTrrUNA2EYE3MBmEDt2tQv1joVP1Q3oDisZ5nw0MZDx2kcgEXM+/kZpfa/PAkFvYVhRUZwytIQWoN3V/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@egjs/list-differ": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@egjs/list-differ/-/list-differ-1.0.1.tgz",
|
||||
"integrity": "sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@element-plus/icons-vue": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
|
||||
@@ -1224,6 +1267,34 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@scena/dragscroll": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz",
|
||||
"integrity": "sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.6.0",
|
||||
"@scena/event-emitter": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@scena/event-emitter": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz",
|
||||
"integrity": "sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@scena/matrix": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@scena/matrix/-/matrix-1.1.1.tgz",
|
||||
"integrity": "sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@simonwep/pickr": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
|
||||
@@ -2904,6 +2975,52 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/croact": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/croact/-/croact-1.0.4.tgz",
|
||||
"integrity": "sha512-9GhvyzTY/IVUrMQ2iz/mzgZ8+NcjczmIo/t4FkC1CU0CEcau6v6VsEih4jkTa4ZmRgYTF0qXEZLObCzdDFplpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@egjs/list-differ": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/croact-css-styled": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/croact-css-styled/-/croact-css-styled-1.1.9.tgz",
|
||||
"integrity": "sha512-G7yvRiVJ3Eoj0ov2h2xR4312hpOzATay2dGS9clK8yJQothjH1sBXIyvOeRP5wBKD9mPcKcoUXPCPsl0tQog4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"css-styled": "~1.0.8",
|
||||
"framework-utils": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/croact-moveable": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/croact-moveable/-/croact-moveable-0.9.0.tgz",
|
||||
"integrity": "sha512-fc3bieV6CdqqZFtzsSLi9KmvUMFW3oakUfhPCls1BxKjOfUfn8rktteGED2341A/Qghy8tI3Hm6SdocIc68IKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@egjs/agent": "^2.2.1",
|
||||
"@egjs/children-differ": "^1.0.1",
|
||||
"@egjs/list-differ": "^1.0.0",
|
||||
"@scena/dragscroll": "^1.4.0",
|
||||
"@scena/event-emitter": "^1.0.5",
|
||||
"@scena/matrix": "^1.1.1",
|
||||
"croact-css-styled": "^1.1.9",
|
||||
"css-to-mat": "^1.1.1",
|
||||
"framework-utils": "^1.1.0",
|
||||
"gesto": "^1.19.3",
|
||||
"overlap-area": "^1.1.0",
|
||||
"react-css-styled": "^1.1.9",
|
||||
"react-moveable": "~0.56.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"croact": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -3014,6 +3131,25 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/css-styled": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/css-styled/-/css-styled-1.0.8.tgz",
|
||||
"integrity": "sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-to-mat": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/css-to-mat/-/css-to-mat-1.1.1.tgz",
|
||||
"integrity": "sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@scena/matrix": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-1.1.3.tgz",
|
||||
@@ -4354,6 +4490,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/framework-utils": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/framework-utils/-/framework-utils-1.1.0.tgz",
|
||||
"integrity": "sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@@ -4485,6 +4627,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gesto": {
|
||||
"version": "1.19.4",
|
||||
"resolved": "https://registry.npmmirror.com/gesto/-/gesto-1.19.4.tgz",
|
||||
"integrity": "sha512-hfr/0dWwh0Bnbb88s3QVJd1ZRJeOWcgHPPwmiH6NnafDYvhTsxg+SLYu+q/oPNh9JS3V+nlr6fNs8kvPAtcRDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@scena/event-emitter": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -5695,6 +5847,24 @@
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keycode": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/keycode/-/keycode-2.2.1.tgz",
|
||||
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keycon": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/keycon/-/keycon-1.4.0.tgz",
|
||||
"integrity": "sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfcs/core": "^0.0.6",
|
||||
"@daybrush/utils": "^1.7.1",
|
||||
"@scena/event-emitter": "^1.0.2",
|
||||
"keycode": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -6212,6 +6382,19 @@
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/moveable": {
|
||||
"version": "0.53.0",
|
||||
"resolved": "https://registry.npmmirror.com/moveable/-/moveable-0.53.0.tgz",
|
||||
"integrity": "sha512-71jS9zIoQzMhnNvduhg4tUEdm23+fO/40FN7muVMbZvVwbTku2MIxxLhnU4qFvxI4oVxn75l79SbtgjuA+s7Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@scena/event-emitter": "^1.0.5",
|
||||
"croact": "^1.0.4",
|
||||
"croact-moveable": "~0.9.0",
|
||||
"react-moveable": "~0.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
@@ -6650,6 +6833,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/overlap-area": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/overlap-area/-/overlap-area-1.1.0.tgz",
|
||||
"integrity": "sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -7037,6 +7229,46 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-css-styled": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmmirror.com/react-css-styled/-/react-css-styled-1.1.9.tgz",
|
||||
"integrity": "sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-styled": "~1.0.8",
|
||||
"framework-utils": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-moveable": {
|
||||
"version": "0.56.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-moveable/-/react-moveable-0.56.0.tgz",
|
||||
"integrity": "sha512-FmJNmIOsOA36mdxbrc/huiE4wuXSRlmon/o+/OrfNhSiYYYL0AV5oObtPluEhb2Yr/7EfYWBHTxF5aWAvjg1SA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@egjs/agent": "^2.2.1",
|
||||
"@egjs/children-differ": "^1.0.1",
|
||||
"@egjs/list-differ": "^1.0.0",
|
||||
"@scena/dragscroll": "^1.4.0",
|
||||
"@scena/event-emitter": "^1.0.5",
|
||||
"@scena/matrix": "^1.1.1",
|
||||
"css-to-mat": "^1.1.1",
|
||||
"framework-utils": "^1.1.0",
|
||||
"gesto": "^1.19.3",
|
||||
"overlap-area": "^1.1.0",
|
||||
"react-css-styled": "^1.1.9",
|
||||
"react-selecto": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-selecto": {
|
||||
"version": "1.26.3",
|
||||
"resolved": "https://registry.npmmirror.com/react-selecto/-/react-selecto-1.26.3.tgz",
|
||||
"integrity": "sha512-Ubik7kWSnZyQEBNro+1k38hZaI1tJarE+5aD/qsqCOA1uUBSjgKVBy3EWRzGIbdmVex7DcxznFZLec/6KZNvwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"selecto": "~1.26.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
@@ -7507,6 +7739,24 @@
|
||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/selecto": {
|
||||
"version": "1.26.3",
|
||||
"resolved": "https://registry.npmmirror.com/selecto/-/selecto-1.26.3.tgz",
|
||||
"integrity": "sha512-gZHgqMy5uyB6/2YDjv3Qqaf7bd2hTDOpPdxXlrez4R3/L0GiEWDCFaUfrflomgqdb3SxHF2IXY0Jw0EamZi7cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@daybrush/utils": "^1.13.0",
|
||||
"@egjs/children-differ": "^1.0.1",
|
||||
"@scena/dragscroll": "^1.4.0",
|
||||
"@scena/event-emitter": "^1.0.5",
|
||||
"css-styled": "^1.0.8",
|
||||
"css-to-mat": "^1.1.1",
|
||||
"framework-utils": "^1.1.0",
|
||||
"gesto": "^1.19.4",
|
||||
"keycon": "^1.2.0",
|
||||
"overlap-area": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
|
||||
@@ -9765,6 +10015,16 @@
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue3-moveable": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmmirror.com/vue3-moveable/-/vue3-moveable-0.28.0.tgz",
|
||||
"integrity": "sha512-vplQO0XkxVEtXMDh2/lZE+c5kMycGXAfYFMvbwFKi8UVYzVk8MTgVHr4fxO9Z+4i4Rb+U/IEIgkhHRMAbx8FJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framework-utils": "^1.1.0",
|
||||
"moveable": "~0.53.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-i18n": "^9.6.1",
|
||||
"vue-router": "^4.0.3",
|
||||
"vue3-moveable": "^0.28.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.0",
|
||||
"x-sender": "^1.1.6"
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: 5.同步 dist 目录到 S3
|
||||
run: |
|
||||
aws s3 sync dist/* s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
||||
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
||||
|
||||
- name: 6.部署完成
|
||||
run: echo "构建和部署到 S3 任务完成。"
|
||||
BIN
public/image/toolsGuide/detailCN.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
public/image/toolsGuide/detailEN.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
23
src/App.vue
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
<div class="loading" v-show="loading"><a-spin :delay="0.5" /></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
const store = useStore();
|
||||
const loading = computed(() => store.state.loading || store.state.view_loading);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
@@ -9,7 +18,19 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999999999999;
|
||||
color: #fff;
|
||||
}
|
||||
.ipad{
|
||||
*{
|
||||
-webkit-touch-callout:none;
|
||||
|
||||
@@ -54,6 +54,24 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">混合模式</div>
|
||||
<div class="code-name">&#xe7a4;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">更多</div>
|
||||
<div class="code-name">&#xe60f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">平铺</div>
|
||||
<div class="code-name">&#xe8d7;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">裁剪</div>
|
||||
@@ -276,9 +294,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
||||
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -304,6 +322,33 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-hunhemoshi"></span>
|
||||
<div class="name">
|
||||
混合模式
|
||||
</div>
|
||||
<div class="code-name">.icon-hunhemoshi
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-gengduo"></span>
|
||||
<div class="name">
|
||||
更多
|
||||
</div>
|
||||
<div class="code-name">.icon-gengduo
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-repeat"></span>
|
||||
<div class="name">
|
||||
平铺
|
||||
</div>
|
||||
<div class="code-name">.icon-repeat
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-caijian"></span>
|
||||
<div class="name">
|
||||
@@ -637,6 +682,30 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-hunhemoshi"></use>
|
||||
</svg>
|
||||
<div class="name">混合模式</div>
|
||||
<div class="code-name">#icon-hunhemoshi</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-gengduo"></use>
|
||||
</svg>
|
||||
<div class="name">更多</div>
|
||||
<div class="code-name">#icon-gengduo</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-repeat"></use>
|
||||
</svg>
|
||||
<div class="name">平铺</div>
|
||||
<div class="code-name">#icon-repeat</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-caijian"></use>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4292253 */
|
||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
||||
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,18 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-hunhemoshi:before {
|
||||
content: "\e7a4";
|
||||
}
|
||||
|
||||
.icon-gengduo:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
|
||||
.icon-repeat:before {
|
||||
content: "\e8d7";
|
||||
}
|
||||
|
||||
.icon-caijian:before {
|
||||
content: "\e650";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,27 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42604348",
|
||||
"name": "混合模式",
|
||||
"font_class": "hunhemoshi",
|
||||
"unicode": "e7a4",
|
||||
"unicode_decimal": 59300
|
||||
},
|
||||
{
|
||||
"icon_id": "45981931",
|
||||
"name": "更多",
|
||||
"font_class": "gengduo",
|
||||
"unicode": "e60f",
|
||||
"unicode_decimal": 58895
|
||||
},
|
||||
{
|
||||
"icon_id": "17005660",
|
||||
"name": "平铺",
|
||||
"font_class": "repeat",
|
||||
"unicode": "e8d7",
|
||||
"unicode_decimal": 59607
|
||||
},
|
||||
{
|
||||
"icon_id": "22138606",
|
||||
"name": "裁剪",
|
||||
|
||||
17
src/assets/icons/CBrush2.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="201.000000pt" height="200.000000pt" viewBox="0 0 201.000000 200.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1654 1990 c-26 -8 -153 -110 -162 -129 -1 -3 81 -89 183 -191 l185
|
||||
-185 56 55 c30 30 62 67 70 82 22 41 18 123 -8 176 -29 57 -126 158 -169 176
|
||||
-47 20 -118 27 -155 16z"/>
|
||||
<path d="M868 1243 c-525 -527 -498 -492 -568 -725 -54 -179 -59 -219 -30
|
||||
-248 19 -19 29 -21 64 -16 113 18 361 105 431 152 28 18 251 235 498 481 l447
|
||||
448 -188 188 -187 187 -467 -467z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
17
src/assets/icons/CEraser2.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M757 1273 c-214 -214 -402 -406 -418 -427 -32 -43 -37 -82 -15 -124
|
||||
23 -44 327 -338 376 -363 l45 -24 463 -3 462 -3 0 46 0 45 -342 0 -343 0 342
|
||||
343 c187 188 346 352 352 364 16 32 13 74 -7 105 -9 14 -110 116 -224 227
|
||||
l-207 201 -47 0 -48 0 -389 -387z m207 -342 l209 -209 -128 -131 c-76 -78
|
||||
-144 -138 -169 -151 -51 -24 -106 -26 -149 -4 -41 20 -337 314 -337 334 0 13
|
||||
347 370 360 370 3 0 99 -94 214 -209z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 847 B |
20
src/assets/icons/CMarquee.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M250 1545 l0 -205 80 0 80 0 0 -340 0 -340 -80 0 -80 0 0 -205 0
|
||||
-205 205 0 205 0 0 80 0 80 340 0 340 0 0 -80 0 -80 205 0 205 0 0 205 0 205
|
||||
-80 0 -80 0 0 340 0 340 80 0 80 0 0 205 0 205 -205 0 -205 0 0 -80 0 -80
|
||||
-340 0 -340 0 0 80 0 80 -205 0 -205 0 0 -205z m320 0 l0 -125 -120 0 -120 0
|
||||
0 125 0 125 120 0 120 0 0 -125z m1100 0 l0 -125 -120 0 -120 0 0 125 0 125
|
||||
120 0 120 0 0 -125z m-330 -125 l0 -80 85 0 85 0 0 -340 0 -340 -85 0 -85 0 0
|
||||
-80 0 -80 -340 0 -340 0 0 80 0 80 -85 0 -85 0 0 340 0 340 85 0 85 0 0 80 0
|
||||
80 340 0 340 0 0 -80z m-770 -965 l0 -125 -120 0 -120 0 0 125 0 125 120 0
|
||||
120 0 0 -125z m1100 0 l0 -125 -120 0 -120 0 0 125 0 125 120 0 120 0 0 -125z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
68
src/assets/icons/CPart.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="103.000000pt" height="92.000000pt" viewBox="0 0 103.000000 92.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,92.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M365 895 c-5 -2 -36 -6 -67 -10 -45 -5 -58 -10 -59 -23 0 -11 -2 -12
|
||||
-6 -4 -7 17 -32 15 -40 -4 -4 -11 -8 -12 -13 -4 -5 8 -13 9 -21 4 -7 -4 -22
|
||||
-9 -34 -10 -11 -1 -41 -11 -66 -21 l-47 -18 20 -63 c11 -38 23 -60 30 -56 6 4
|
||||
8 -1 3 -15 -3 -11 -3 -21 2 -21 4 0 9 -12 9 -26 1 -14 5 -28 8 -32 4 -3 33 3
|
||||
66 15 33 11 61 19 62 18 2 -2 1 -139 -2 -304 l-5 -301 309 2 309 3 -2 108 c-2
|
||||
77 2 113 11 125 10 12 10 14 1 8 -7 -4 -13 -2 -13 4 0 6 10 8 23 5 18 -5 26
|
||||
-1 36 17 9 17 10 18 6 3 -5 -16 -4 -18 6 -7 18 17 -1 34 -40 37 l-32 2 -2 151
|
||||
c-1 82 0 147 3 144 9 -12 44 -11 52 1 5 8 8 7 8 -4 0 -14 26 -28 52 -29 11 0
|
||||
39 70 33 80 -3 5 1 11 7 13 10 4 9 8 -2 16 -13 10 -8 15 12 12 8 -1 38 91 32
|
||||
98 -2 2 -10 -2 -18 -8 -9 -7 -17 -8 -20 -2 -3 5 0 11 6 14 7 2 -18 14 -56 26
|
||||
-38 12 -71 19 -74 16 -3 -3 -11 0 -18 6 -8 6 -20 9 -28 6 -8 -3 -16 -2 -18 3
|
||||
-5 15 -154 30 -286 29 -70 0 -131 -2 -137 -4z m21 -30 c-6 -18 3 -47 14 -40 4
|
||||
2 18 -7 30 -20 27 -29 17 -34 -14 -7 -20 16 -20 16 -7 -1 23 -29 66 -47 112
|
||||
-47 34 0 40 3 35 16 -5 14 -4 15 9 4 13 -11 19 -9 36 6 12 11 29 35 38 54 15
|
||||
31 20 35 58 35 24 0 43 -2 43 -5 0 -11 115 -27 121 -18 3 5 9 2 13 -7 4 -13
|
||||
14 -16 34 -12 15 2 36 1 47 -4 16 -7 14 -8 -10 -5 l-30 5 32 -14 c39 -18 39
|
||||
-19 14 -83 -15 -38 -17 -52 -8 -61 9 -9 8 -11 -7 -5 -16 6 -18 4 -12 -19 3
|
||||
-14 2 -29 -4 -32 -6 -3 -7 1 -4 9 6 16 -9 20 -73 19 -12 0 -20 4 -17 8 3 5 -4
|
||||
9 -15 9 -20 0 -21 -5 -21 -151 l0 -151 -27 8 c-16 4 -38 7 -50 7 -19 -1 -21 2
|
||||
-13 17 15 28 12 57 -6 57 -11 0 -15 -8 -12 -27 4 -24 1 -27 -23 -26 -15 1 -25
|
||||
4 -22 9 2 4 -2 7 -10 7 -9 0 -14 -10 -14 -25 0 -13 3 -22 8 -19 5 3 6 -1 3 -9
|
||||
-2 -7 0 -25 5 -40 8 -20 7 -25 -3 -21 -7 3 -13 -2 -13 -11 0 -12 7 -15 26 -11
|
||||
14 3 21 3 14 0 -7 -3 -9 -12 -6 -20 3 -7 10 -11 16 -7 5 3 7 1 4 -4 -9 -14 3
|
||||
-74 12 -68 4 2 7 -6 7 -18 -1 -32 32 -34 44 -3 5 14 7 34 5 46 -3 13 0 18 7
|
||||
14 8 -5 9 -1 5 10 -4 9 -3 15 2 12 5 -3 12 1 15 10 3 8 2 12 -4 9 -6 -3 -10
|
||||
-1 -10 4 0 13 3 13 24 5 13 -5 16 -24 16 -103 0 -63 4 -102 13 -111 10 -12 9
|
||||
-12 -4 -2 -13 10 -88 12 -300 10 l-284 -3 3 303 3 302 -26 0 c-14 0 -25 -4
|
||||
-25 -10 0 -5 -7 -6 -17 -3 -9 4 -14 2 -10 -3 4 -6 -6 -9 -24 -6 -28 4 -41 -11
|
||||
-19 -21 6 -3 5 -4 -2 -3 -7 2 -13 11 -13 22 -1 32 -25 104 -35 104 -5 0 -6 7
|
||||
-3 17 4 10 2 14 -5 9 -7 -4 -10 2 -8 17 3 29 14 47 29 47 7 0 3 -8 -8 -17
|
||||
l-20 -16 20 8 c11 4 28 10 37 12 10 3 18 9 18 13 0 4 6 7 12 7 22 -2 158 26
|
||||
158 32 0 3 20 6 45 5 25 0 45 3 45 8 0 4 3 8 6 8 3 0 4 -7 0 -15z m237 -5 c-3
|
||||
-9 1 -8 11 4 9 11 16 15 16 9 0 -6 -7 -16 -15 -23 -8 -7 -15 -9 -15 -4 0 10
|
||||
-29 -28 -30 -40 0 -4 8 -5 17 -2 15 6 15 4 -2 -14 -22 -24 -37 -26 -28 -4 5
|
||||
14 3 15 -15 5 -26 -14 -77 -14 -103 -1 -10 6 -27 27 -38 48 l-19 37 113 0 c95
|
||||
0 112 -2 108 -15z"/>
|
||||
<path d="M346 641 c-3 -5 1 -12 10 -15 23 -9 36 -7 29 4 -3 6 1 7 9 4 9 -3 16
|
||||
-1 16 5 0 13 -56 15 -64 2z"/>
|
||||
<path d="M440 640 c0 -16 33 -26 38 -12 2 7 8 10 13 6 5 -3 9 0 9 5 0 6 -13
|
||||
11 -30 11 -16 0 -30 -5 -30 -10z"/>
|
||||
<path d="M530 641 c0 -12 37 -24 50 -16 20 12 10 25 -20 25 -16 0 -30 -4 -30
|
||||
-9z"/>
|
||||
<path d="M620 641 c0 -12 37 -24 50 -16 20 12 10 25 -20 25 -16 0 -30 -4 -30
|
||||
-9z"/>
|
||||
<path d="M310 593 c0 -20 5 -30 16 -30 10 0 14 8 12 25 -4 37 -28 40 -28 5z"/>
|
||||
<path d="M697 613 c-13 -13 -7 -50 8 -50 10 0 15 10 15 29 0 27 -9 35 -23 21z"/>
|
||||
<path d="M317 534 c-4 -4 -7 -20 -7 -36 0 -35 23 -34 28 1 4 25 -10 46 -21 35z"/>
|
||||
<path d="M692 503 c4 -39 28 -42 28 -4 0 21 -5 31 -16 31 -11 0 -14 -8 -12
|
||||
-27z"/>
|
||||
<path d="M312 415 c4 -33 22 -33 26 0 2 18 -1 25 -13 25 -12 0 -15 -7 -13 -25z"/>
|
||||
<path d="M312 329 c2 -19 8 -33 13 -31 15 3 12 55 -3 60 -10 3 -13 -5 -10 -29z"/>
|
||||
<path d="M342 278 c3 -7 19 -14 37 -16 24 -3 32 0 29 10 -3 7 -19 14 -37 16
|
||||
-24 3 -32 0 -29 -10z"/>
|
||||
<path d="M443 275 c0 -10 10 -15 29 -15 18 0 28 5 28 15 0 10 -10 15 -28 15
|
||||
-19 0 -29 -5 -29 -15z"/>
|
||||
<path d="M530 275 c0 -10 10 -15 33 -15 22 0 28 3 18 9 -11 7 -11 9 0 14 8 3
|
||||
-1 6 -18 6 -23 1 -33 -4 -33 -14z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
31
src/assets/icons/CPoint.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M346 936 c-21 -13 -49 -41 -62 -62 -41 -67 -27 -180 27 -218 47 -32
|
||||
53 -12 12 40 -71 94 -2 229 116 229 37 0 58 -7 87 -28 32 -22 40 -24 42 -12
|
||||
14 64 -142 100 -222 51z"/>
|
||||
<path d="M368 877 c-33 -28 -48 -57 -48 -96 0 -39 9 -61 26 -61 10 0 14 13 14
|
||||
46 0 57 12 79 50 93 61 21 110 -22 110 -96 0 -48 14 -56 31 -19 31 67 -35 156
|
||||
-114 156 -29 0 -50 -7 -69 -23z"/>
|
||||
<path d="M580 794 c0 -58 -9 -84 -43 -121 -19 -21 -19 -23 -2 -29 34 -13 85
|
||||
75 85 148 0 37 -10 58 -26 58 -10 0 -14 -15 -14 -56z"/>
|
||||
<path d="M410 749 c-13 -6 -28 -15 -32 -22 -4 -7 -8 -106 -8 -222 l-1 -210
|
||||
-27 34 c-62 80 -89 101 -126 101 -27 0 -39 -6 -52 -25 -15 -24 -15 -28 0 -68
|
||||
21 -53 78 -123 94 -113 18 11 16 17 -28 75 -44 58 -48 72 -25 91 21 18 54 -6
|
||||
93 -68 31 -47 74 -78 93 -67 5 4 9 101 9 224 0 225 3 241 42 241 10 0 19 -1
|
||||
19 -2 1 -2 5 -86 8 -188 5 -165 8 -185 24 -188 15 -3 17 5 17 61 0 72 17 100
|
||||
45 77 10 -9 15 -32 15 -77 0 -56 2 -64 18 -61 12 2 16 11 14 34 -5 48 14 86
|
||||
41 82 20 -3 22 -9 25 -66 3 -53 6 -63 20 -60 12 2 19 16 22 43 4 32 10 41 27
|
||||
43 36 5 46 -36 39 -166 -6 -127 -19 -160 -75 -195 -48 -29 -176 -34 -246 -10
|
||||
-48 18 -56 25 -130 128 -16 22 -25 26 -37 19 -15 -8 -11 -18 31 -75 78 -105
|
||||
94 -113 236 -117 119 -3 121 -3 167 27 69 44 82 74 86 214 6 179 -8 221 -72
|
||||
216 -22 -2 -39 4 -55 20 -17 17 -32 22 -55 19 -19 -2 -38 2 -45 9 -6 7 -25 13
|
||||
-40 13 l-29 0 -4 100 c-3 103 -11 122 -53 133 -11 3 -31 1 -45 -4z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
9
src/assets/icons/overallMore.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="24" height="24" fill="url(#pattern0_2641_12790)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_2641_12790" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_2641_12790" transform="scale(0.0078125)"/>
|
||||
</pattern>
|
||||
<image id="image0_2641_12790" width="128" height="128" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAACIlJREFUeJztnWlsFVUUgL9OyxNwoVUoCBZFVNxQUaNWo0aiica4SxTUGJe4x+0PBJc/rlH8oSYucQvgQkARixpCECNqgtGKiiigssbK5sLeCvT54/TR+mz75s7cuXfeeL7k/Gpn3jlzZ+5y7rnngKIo/18qfCuQcfYGBgF922QzsA5YD6zxqJeSEBXAWcAEoBHYBeS7kFXAROAaoLcPZRV75IAbgYV03eDdye/AY8BA14or8akHFhGt4YtlKzAWqHRqgRKJAHiC7rv5qPIZsL87UxRT9gCmYL/hO8py4HBXBinhyQFzSbbxC7IBONKNWUpYXsRN43fsCfo6sUwpya24bfyCzEZ9Nt7pD2zEzwuQB65N3kSlOybhr/HziOewj22jdL0ZjiOQsd+0G94MvAe8grxAHwM/A7VAjeG99gK2AJ8aXqdYYAJmX+tO4HGguov7VQBjgCbD+y5D/A+KQ3oAawnfSFuBc0PeeyCwwODeeeAcCzZ1yjHAm8hY43OsK3cZZfjcB2LeE0SV34A3gOHFSowBWlLw8Mpdphc/2JBc5VjPFmB04cePQRvflpxENAJghWNdW4DhATAOcW8q8WgCvox4bSsw06IuYcgB4wJgpOMfzipLkS8rKottKWLAyICulyqKGZtjXr/RihZmVBfGHiU+tTGvH2BFCzOWB8BUDz+cRYYTL7bvFFuKGDANxL/8M/5n0FmQ3UsrQ2qQIcSlrj8B+xQUGIK5R0rlv7KEaCuqpxzr2Yi0+b82NyqBC5CwZo1HaycHXGTw/68gEcJhOR9oILyPvxHZE4jCb8iGVAOy9FRC0ojZF/Yy4XqC0cA2g/u2AodYskkx4DbMu9mliIu9eGIYIBO+hgj3nJuQfUoJqoE/iDbebgPmI3EB85BuOOrYfWnShipdcyd+JpYF+RSNC/RKFfAdfhp/J3Bs8iYqpTgWCcty/QKMdWGcEo5LkNm4q8afinb9qeMukjkTWCwfAr0c2aQYcimwneQafxISj6ikmFOBH7Hb8FuQHka7/TKhJ/AwdnqDD4CDnGqvWKMfMltfjVmjtyATvZPdq6zdTBJUAscjm2qnA4ORl6MvsAlJELUW+ArZmJlH/GgiRVEURVEURQlPGlYBVciRqhOR2XItkmJ1AzJjXo1shf7iS0HFPgESZ/c+4aNhVwLPoVmzypoAuIl4YeitwCykx1DKiKHAR9jzme9C0rbs5dIIJRqXYRb9aiKLEG+bklLuIPm98ybgOFcGKeG5iWQbvqNsoO20i5IOTgWacfcC5IEf6HDeTfFHDf6STU12YJ9Sgmfx0/gFOTN5E5WuOBrYgd8X4Gs0oaI3XsO8wf4CngROQ1zBOaAOybk3nWhh2GGTNSoW2QfzgxNTgP1K3PckZC/A5L5v2zMruxRvBvVCXKxR89XUA/cY/P/Tbf+fD/G/fYFPCL8X8DdwHTIcKcIaJBRte/EfKoEHcZumZA7m2cqH4udoVpZkE/BAx2dfAbzlWIlddJKvNiQPpeAhZkHepG0EcJ2nNk+8nPd1uD2bl2UZEwA3mz1/K8yJce1q/GTVzCI3B8AIDz+8Mub1q6xooYzw5SyJ+7tpCGXLAvkAyQ/omjrP1yvCggB43sMPxyl7MhgtqWqLF0C60zdwO/vchRSqiMLDjnXNqrxOh6G0ErgfcRK4UmAu5o6gQ5GiTL4fXjnLRmB84dkXT6Z6Iq7gqKliTV3BzwB3tylWCnUFxyNPuyu4OakfibIZNJXSxZHrkfy4JvedZs8sxYRXidYtTUDO0/dHtoMHA1cAM9Dt4LLiKPwHhDSiASFeeRp/jd8KnJG8iUp31BAvKXIcmejAPiUE9bgPC/8G2NOFcUo4bsBd469DU6ylkutJflL4K5pNO9VcTHKHQxcCB7gzRYnKwUggiK2G34GsNnTMLyMqkCGhUGc3irQiKVV9BLIolgiQEnUNhN+AWo4cORvmQd/MkobImipkA+pE5FRQLbKnsL5NVgKfIS+AoiiKoiiKoihxScMqIGtUASfQXjCiDol46kd7wYh1SGjWXKRgxCYvmipWqSVeyZh69yorNugJPIadre9ZiMtcKRNOA5Zgd3NrKxJZrcNzyrmcZAtHTkaCY5UUci9uchTMBno7skkJyWW4TVDxDjocpIbj8JOjaLwL45TuqQK+w33j55HDtFogwzOFdHa+5HMSGApMT+f+X6kG3kXW/KZsQ04qNSIewB5IUSxT6oDvkYzoimNux/yLXQxcyX9n8RVI5tMZEe75cUL2KSX4GrOGeolwa/hRmOU7aEVyJFij45hSiZRyOws5oasIOeS5hOUlpFJKWM4DZhJ+OG5EjspHYS2yAdWATCx3MwQ5VuVzkpMFWUw0792TjvVcQIfyOtWYZ+JW6VyuJBo1uM3TnEdqN/Yp5AYy6eKUztkG3Ei0lDTNwPG4rYq6L7AjQDY1lPgsRF6CqHxhSxEDRgXoiVpbrI15/RorWpgxJEDKtSjxiVuuro8VLcz4M0Dq+SrxGUY8V62P7KcfgRRuaMH/DDoLcrLR428nAFY41rUZqfAGwGj0JbAhM4jG1Y71bKaTJetwJGdwk6eHlxW5ovjBlmAQ7hJqNSE5gnd/+UppeiAz/LAPeSvi3g3DIMw9sGdbsEkxZAJmjbQTce/WdHG/AOn2Tb/8ZVhOgKmxZuE4AliE+fPagsT6z0fW+TXAYcCFwIER9LgPeDTCdYoFJuJ3brGG+L4GJQb9EaeZrxfgmuRNVEpxC34afxY6XKeGF3Db+MsoXU9BcUgOu/kOu5N1yARUSRk5kq+3vBxNiZdqAuBxZM1vu/HnAQPcmaLEYQTmUcNdyRYkyYRWOykzckiFsm+J1vDrgUfQrz4TnAk8AXxJ98PDCuA14Cqglw9FQdeWSbMnksq+H7KU24TM7AuiKIrikX8A+4ThOTuVZbQAAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/images/award/arrow.png
Normal file
|
After Width: | Height: | Size: 198 B |
BIN
src/assets/images/award/bloom_bg.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
src/assets/images/award/bloom_logo.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/award/code_create_logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/images/award/design_bg.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
src/assets/images/award/timeline_bg.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/images/award/timeline_line.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/assets/images/award/∞.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
src/assets/images/canvas/add.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/images/canvas/remove.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/canvas/shubiao-l.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/canvas/shubiao-r.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/images/canvas/xiangao.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
src/assets/images/canvas/xiangaofenge.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
src/assets/images/canvas/yinhua1.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
src/assets/images/icon/xyz.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 313 KiB |
9
src/assets/images/socialMediaLogo/biliBliIcon.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
10
src/assets/images/socialMediaLogo/faceBookIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2427_2565)">
|
||||
<path d="M12 0C5.37264 0 0 5.37264 0 12C0 17.6275 3.87456 22.3498 9.10128 23.6467V15.6672H6.62688V12H9.10128V10.4198C9.10128 6.33552 10.9498 4.4424 14.9597 4.4424C15.72 4.4424 17.0318 4.59168 17.5685 4.74048V8.06448C17.2853 8.03472 16.7933 8.01984 16.1822 8.01984C14.2147 8.01984 13.4544 8.76528 13.4544 10.703V12H17.3741L16.7006 15.6672H13.4544V23.9122C19.3963 23.1946 24.0005 18.1354 24.0005 12C24 5.37264 18.6274 0 12 0Z" fill="#2C2C2C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2427_2565">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
|
Before Width: | Height: | Size: 370 KiB |
10
src/assets/images/socialMediaLogo/linkedinIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2427_2564)">
|
||||
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5563 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2938 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516V20.4516Z" fill="#2C2C2C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2427_2564">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 997 B |
10
src/assets/images/socialMediaLogo/socialIcons.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2426_2350)">
|
||||
<path d="M23.5233 7.12823C23.5233 7.12823 23.2913 5.49009 22.5766 4.77079C21.6717 3.8241 20.6601 3.81946 20.196 3.76377C16.8733 3.52246 11.8846 3.52246 11.8846 3.52246H11.8754C11.8754 3.52246 6.88669 3.52246 3.564 3.76377C3.09994 3.81946 2.08828 3.8241 1.18336 4.77079C0.468703 5.49009 0.241312 7.12823 0.241312 7.12823C0.241312 7.12823 0 9.05409 0 10.9753V12.7759C0 14.6971 0.236672 16.6229 0.236672 16.6229C0.236672 16.6229 0.468703 18.2611 1.17872 18.9804C2.08364 19.9271 3.27164 19.8946 3.80067 19.9967C5.70333 20.1777 11.88 20.2334 11.88 20.2334C11.88 20.2334 16.8733 20.2241 20.196 19.9874C20.6601 19.9317 21.6717 19.9271 22.5766 18.9804C23.2913 18.2611 23.5233 16.6229 23.5233 16.6229C23.5233 16.6229 23.76 14.7017 23.76 12.7759V10.9753C23.76 9.05409 23.5233 7.12823 23.5233 7.12823ZM9.42511 14.9616V8.28374L15.8431 11.6343L9.42511 14.9616Z" fill="#2C2C2C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2426_2350">
|
||||
<rect width="23.76" height="23.76" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 68 KiB |
3
src/assets/images/socialMediaLogo/tikTokIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.9218 0H10.9175V16.1843C10.9175 18.1127 9.37747 19.6967 7.4609 19.6967C5.54433 19.6967 4.00424 18.1127 4.00424 16.1843C4.00424 14.2905 5.51011 12.7408 7.35825 12.672V8.60871C3.28553 8.67755 0 12.0177 0 16.1843C0 20.3854 3.35398 23.76 7.49514 23.76C11.6362 23.76 14.9902 20.351 14.9902 16.1843V7.88555C16.4961 8.98748 18.3442 9.64174 20.295 9.67619V5.61287C17.2833 5.50957 14.9218 3.03026 14.9218 0Z" fill="#2C2C2C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
|
Before Width: | Height: | Size: 191 KiB |
9
src/assets/images/socialMediaLogo/xiaoHongShuIcon.svg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 299 KiB |
@@ -1250,10 +1250,14 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
}
|
||||
.ant-spin .ant-spin-dot {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
.ant-spin-dot-item {
|
||||
background-color: #000000 !important;
|
||||
width: 9px !important;
|
||||
height: 9px !important;
|
||||
width: 0.9em !important;
|
||||
height: 0.9em !important;
|
||||
}
|
||||
.ant-spin {
|
||||
color: #000;
|
||||
@@ -1358,7 +1362,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
||||
}
|
||||
.admin_page .admin_state_item > span {
|
||||
white-space: nowrap;
|
||||
width: 13rem;
|
||||
min-width: 13rem;
|
||||
}
|
||||
.admin_page .admin_state_item > span > span {
|
||||
color: red;
|
||||
|
||||
@@ -1378,10 +1378,14 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
||||
}
|
||||
}
|
||||
//loding样式
|
||||
.ant-spin .ant-spin-dot{
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
.ant-spin-dot-item{
|
||||
background-color: #000000 !important;
|
||||
width: 9px !important;
|
||||
height: 9px !important;
|
||||
width: .9em !important;
|
||||
height: .9em !important;
|
||||
}
|
||||
.ant-spin{
|
||||
color: #000;
|
||||
@@ -1490,7 +1494,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
||||
align-items: center;
|
||||
>span{
|
||||
white-space: nowrap;
|
||||
width: 13rem;
|
||||
min-width: 13rem;
|
||||
>span{
|
||||
color: red;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
<div class="generalModel_btn">
|
||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
||||
<svg
|
||||
width="100%" height="100%"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 46 46"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -49,7 +50,10 @@
|
||||
</div>
|
||||
<div class="allUserPoeration_center admin_page">
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t('admin.UserName') }}: <span>*</span></span>
|
||||
<span>
|
||||
{{ $t('admin.UserName') }}:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<input
|
||||
v-model="userName"
|
||||
:placeholder="$t('admin.enterUserName')"
|
||||
@@ -58,7 +62,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t('admin.UserEmail') }}: <span>*</span></span>
|
||||
<span>
|
||||
{{ $t('admin.UserEmail') }}:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<input
|
||||
v-model="userEmail"
|
||||
:placeholder="$t('admin.enterEmail')"
|
||||
@@ -67,7 +74,10 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t('admin.Password') }}: <span>*</span></span>
|
||||
<span>
|
||||
{{ $t('admin.Password') }}:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<input
|
||||
@focus="focus"
|
||||
@blur="blur"
|
||||
@@ -86,6 +96,19 @@
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="admin_state_item" v-if="title?.value == 'Edit'">
|
||||
<span>
|
||||
{{ $t('admin.SubscribePlan') }}:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-select
|
||||
v-model:value="subscriptionPlanId"
|
||||
style="width: 250px"
|
||||
:options="activePlanOptions"
|
||||
:field-names="{ label: 'name', value: 'id' }"
|
||||
:placeholder="$t('admin.SelectPlan')"
|
||||
></a-select>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="allUserPoeration_btn admin_page">
|
||||
<div class="admin_search_item" @click="cancelDsign">{{ $t('admin.Close') }}</div>
|
||||
@@ -96,7 +119,7 @@
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
@@ -105,90 +128,114 @@ import {
|
||||
onMounted,
|
||||
nextTick,
|
||||
toRefs,
|
||||
} from "vue";
|
||||
import { Https } from "@/tool/https";
|
||||
import { Modal, message } from "ant-design-vue";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
||||
import { formatTime, isEmail } from "@/tool/util";
|
||||
import md5 from "md5";
|
||||
computed
|
||||
} from 'vue'
|
||||
import { Https } from '@/tool/https'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { formatTime, isEmail } from '@/tool/util'
|
||||
import md5 from 'md5'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
emits: ["searchHistoryList"],
|
||||
props: {
|
||||
planOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['searchHistoryList'],
|
||||
setup(props, { emit }) {
|
||||
const {t} = useI18n()
|
||||
const { t } = useI18n()
|
||||
const { planOptions } = toRefs(props)
|
||||
// 筛选出状态为 ACTIVE 的订阅计划
|
||||
const activePlanOptions = computed(() => {
|
||||
if (!planOptions.value || !Array.isArray(planOptions.value)) {
|
||||
return []
|
||||
}
|
||||
return planOptions.value.filter((plan: any) => plan.status === 'ACTIVE')
|
||||
})
|
||||
let operations = reactive({
|
||||
operationsModal: false,
|
||||
operationsEdit: false,
|
||||
loadingShow: false,
|
||||
title: null,
|
||||
});
|
||||
title: null
|
||||
})
|
||||
let operationsData = reactive({
|
||||
accountId: -1,
|
||||
userName: "",
|
||||
userEmail: "",
|
||||
password: "",
|
||||
oldPassword: "",
|
||||
credits: "",
|
||||
});
|
||||
userName: '',
|
||||
userEmail: '',
|
||||
password: '',
|
||||
oldPassword: '',
|
||||
credits: '',
|
||||
subscriptionPlanId: '',
|
||||
oldSubscriptionPlanId: ''
|
||||
})
|
||||
let state = ref([
|
||||
{
|
||||
label: "visitor",
|
||||
value: "0",
|
||||
label: 'visitor',
|
||||
value: '0'
|
||||
},
|
||||
{
|
||||
label: "yearly",
|
||||
value: "1",
|
||||
label: 'yearly',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: "monthly",
|
||||
value: "2",
|
||||
label: 'monthly',
|
||||
value: '2'
|
||||
},
|
||||
{
|
||||
label: "trial",
|
||||
value: "3",
|
||||
},
|
||||
]);
|
||||
label: 'trial',
|
||||
value: '3'
|
||||
}
|
||||
])
|
||||
let init = (funStr, data) => {
|
||||
operations.operationsModal = true;
|
||||
operations.operationsEdit = true;
|
||||
operations.title = funStr;
|
||||
if (funStr.value == "Add") operations.operationsEdit = false;
|
||||
if (funStr.value == "Edit") {
|
||||
operationsData.accountId = data.id;
|
||||
operationsData.userName = data.userName;
|
||||
operationsData.userEmail = data.userEmail;
|
||||
operationsData.password = data.userPassword?data.userPassword:null;
|
||||
operationsData.oldPassword = data.userPassword;
|
||||
operations.operationsModal = true
|
||||
operations.operationsEdit = true
|
||||
operations.title = funStr
|
||||
if (funStr.value == 'Add') operations.operationsEdit = false
|
||||
if (funStr.value == 'Edit') {
|
||||
operationsData.accountId = data.id
|
||||
operationsData.userName = data.userName
|
||||
operationsData.userEmail = data.userEmail
|
||||
operationsData.password = data.userPassword ? data.userPassword : null
|
||||
operationsData.oldPassword = data.userPassword
|
||||
// operationsData.validStartTime='2024-08-05T00:00:06'
|
||||
// operationsData.validEndTime='2024-08-05T00:00:06'
|
||||
operationsData.credits = data.creditsUsageLimit;
|
||||
operationsData.credits = data.creditsUsageLimit
|
||||
operationsData.subscriptionPlanId = data.subscriptionPlanId || ''
|
||||
operationsData.oldSubscriptionPlanId = data.subscriptionPlanId || ''
|
||||
// operationsData.accountId = data.accountId
|
||||
// operationsData.userName = data.userName
|
||||
// operationsData.userEmail = data.userEmail
|
||||
// operationsData.validStartTime = formatTime(data.validStartTime)
|
||||
// operationsData.validEndTime = formatTime(data.validEndTime)
|
||||
}
|
||||
};
|
||||
let focus = (event) => {
|
||||
if (funStr.value == 'Add') {
|
||||
operationsData.subscriptionPlanId = ''
|
||||
operationsData.oldSubscriptionPlanId = ''
|
||||
}
|
||||
}
|
||||
let focus = event => {
|
||||
if (operationsData.password == operationsData.oldPassword) {
|
||||
operationsData.password = "";
|
||||
operationsData.password = ''
|
||||
}
|
||||
};
|
||||
let blur = (event) => {
|
||||
console.log(operationsData.password == "" && operationsData.oldPassword);
|
||||
if (operationsData.password == "" && operationsData.oldPassword) {
|
||||
operationsData.password = operationsData.oldPassword;
|
||||
}
|
||||
let blur = event => {
|
||||
console.log(operationsData.password == '' && operationsData.oldPassword)
|
||||
if (operationsData.password == '' && operationsData.oldPassword) {
|
||||
operationsData.password = operationsData.oldPassword
|
||||
}
|
||||
};
|
||||
}
|
||||
let setAddData = () => {
|
||||
return {
|
||||
creditsUsageLimit: operationsData.credits,
|
||||
userEmail: operationsData.userEmail,
|
||||
userPassword: operationsData.password?md5(operationsData.password + "abc"):'',
|
||||
userPassword: operationsData.password ? md5(operationsData.password + 'abc') : '',
|
||||
userName: operationsData.userName,
|
||||
};
|
||||
};
|
||||
subscriptionPlanId: operationsData.subscriptionPlanId
|
||||
}
|
||||
}
|
||||
let setEditData = () => {
|
||||
return {
|
||||
id: operationsData.accountId,
|
||||
@@ -198,57 +245,63 @@ export default defineComponent({
|
||||
userPassword:
|
||||
operationsData.password == operationsData.oldPassword
|
||||
? null
|
||||
: md5(operationsData.password + "abc"),
|
||||
};
|
||||
};
|
||||
let cancelDsign = () => {
|
||||
operationsData.accountId = -1;
|
||||
operationsData.userName = "";
|
||||
operationsData.userEmail = "";
|
||||
operationsData.password = "";
|
||||
operationsData.credits = "";
|
||||
operations.operationsModal = false;
|
||||
};
|
||||
let setOk = () => {
|
||||
let data;
|
||||
if (operations.title?.value == "Add") {
|
||||
data = setAddData();
|
||||
if (!isEmail(data.userEmail)) {
|
||||
message.info(t('admin.jsContent1'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!data.userName ||
|
||||
!data.userEmail ||
|
||||
!data.userPassword
|
||||
)
|
||||
return message.warning(t('admin.jsContent2'));
|
||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(
|
||||
(rv) => {
|
||||
if (rv) {
|
||||
cancelDsign();
|
||||
emit("searchHistoryList");
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
data = setEditData();
|
||||
if (!isEmail(data.userEmail)) {
|
||||
message.info("The email format is incorrect");
|
||||
return;
|
||||
}
|
||||
if (!data.userName || !data.userEmail)
|
||||
return message.warning("Please check the input box marked with *");
|
||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(
|
||||
(rv) => {
|
||||
if (rv) {
|
||||
cancelDsign();
|
||||
emit("searchHistoryList");
|
||||
}
|
||||
}
|
||||
);
|
||||
: md5(operationsData.password + 'abc'),
|
||||
subscriptionPlanId: operationsData.subscriptionPlanId
|
||||
}
|
||||
};
|
||||
}
|
||||
let cancelDsign = () => {
|
||||
operationsData.accountId = -1
|
||||
operationsData.userName = ''
|
||||
operationsData.userEmail = ''
|
||||
operationsData.password = ''
|
||||
operationsData.credits = ''
|
||||
operationsData.subscriptionPlanId = ''
|
||||
operationsData.oldSubscriptionPlanId = ''
|
||||
operations.operationsModal = false
|
||||
}
|
||||
let setOk = () => {
|
||||
let data
|
||||
if (operations.title?.value == 'Add') {
|
||||
data = setAddData()
|
||||
if (!isEmail(data.userEmail)) {
|
||||
message.info(t('admin.jsContent1'))
|
||||
return
|
||||
}
|
||||
if (!data.userName || !data.userEmail || !data.userPassword)
|
||||
return message.warning(t('admin.jsContent2'))
|
||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(rv => {
|
||||
if (rv) {
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
data = setEditData()
|
||||
if (!isEmail(data.userEmail)) {
|
||||
message.info('The email format is incorrect')
|
||||
return
|
||||
}
|
||||
if (!data.userName || !data.userEmail || !data.subscriptionPlanId)
|
||||
return message.warning('Please check the input box marked with *')
|
||||
const needSwitchPlan =
|
||||
operationsData.subscriptionPlanId &&
|
||||
operationsData.subscriptionPlanId !== operationsData.oldSubscriptionPlanId
|
||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(async rv => {
|
||||
if (rv) {
|
||||
if (needSwitchPlan) {
|
||||
await Https.axiosGet(Https.httpUrls.switchSubAccountSubscribePlan, {
|
||||
params: {
|
||||
targetSubscriptionPlanId: operationsData.subscriptionPlanId,
|
||||
subAccId: operationsData.accountId
|
||||
}
|
||||
})
|
||||
}
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
...toRefs(operations),
|
||||
...toRefs(operationsData),
|
||||
@@ -258,14 +311,16 @@ export default defineComponent({
|
||||
focus,
|
||||
blur,
|
||||
setOk,
|
||||
};
|
||||
planOptions,
|
||||
activePlanOptions
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
return {}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {},
|
||||
});
|
||||
methods: {}
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
:deep(.allUserPoeration_modal) {
|
||||
|
||||
@@ -36,27 +36,9 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t("admin.Email") }}:</span>
|
||||
<input
|
||||
v-model="email"
|
||||
:placeholder="$t('admin.enterEmail')"
|
||||
@keydown.enter="gettrialList"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t("admin.UserName") }}:</span>
|
||||
<a-select
|
||||
v-model:value="ids"
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
:filter-option="filterOption"
|
||||
:placeholder="$t('admin.selectUserName')"
|
||||
max-tag-count="responsive"
|
||||
:options="allUserList"
|
||||
@keydown.enter="gettrialList"
|
||||
></a-select>
|
||||
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||
</div>
|
||||
|
||||
<div class="admin_state_item">
|
||||
<span>Organization Name:</span>
|
||||
<input
|
||||
@@ -100,8 +82,9 @@
|
||||
import { useStore } from "vuex";
|
||||
import { Https } from "@/tool/https";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import SelectUser from "@/component/common/SelectUser.vue";
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
components: { SelectUser },
|
||||
setup() {
|
||||
const store: any = useStore();
|
||||
let rangePickerValue: any = ref([]);
|
||||
@@ -176,9 +159,6 @@
|
||||
];
|
||||
});
|
||||
|
||||
let allUserList: any = computed(() => {
|
||||
return store.state.adminPage.allUserList;
|
||||
});
|
||||
let ids = ref([]);
|
||||
let email = ref("");
|
||||
let dataList: any = ref([]);
|
||||
@@ -193,7 +173,6 @@
|
||||
rangeTimeValue,
|
||||
columns,
|
||||
dataList,
|
||||
allUserList,
|
||||
ids,
|
||||
email,
|
||||
renameData,
|
||||
@@ -251,7 +230,7 @@
|
||||
endTime: endDate,
|
||||
startTime: startDate,
|
||||
ids: ids,
|
||||
email: this.email.trim(),
|
||||
email: this.email?.trim(),
|
||||
organizationName: this.organizationName,
|
||||
};
|
||||
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
||||
|
||||
@@ -220,7 +220,7 @@ export default defineComponent({
|
||||
changeEvent:this.changeEvent,
|
||||
size:this.pageSize,
|
||||
page:this.currentPage,
|
||||
email:this.email.trim(),
|
||||
email:this.email?.trim(),
|
||||
}
|
||||
Https.axiosPost(Https.httpUrls.getGenerateFrequency,data).then((rv: any) => {
|
||||
if (rv) {
|
||||
|
||||
@@ -25,15 +25,7 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>{{ $t('admin.UserName') }}:</span>
|
||||
<a-select
|
||||
v-model:value="userIdList"
|
||||
mode="multiple"
|
||||
style="width: 280px"
|
||||
:filter-option="filterOption"
|
||||
:placeholder="$t('admin.selectUserName')"
|
||||
max-tag-count="responsive"
|
||||
:options="dataList"
|
||||
></a-select>
|
||||
<SelectUser v-model="userIdList" labelKey="email" multiple />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -78,16 +70,15 @@ import { LabelLayout } from 'echarts/features';
|
||||
import { useStore } from "vuex";
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SelectUser from '@/component/common/SelectUser.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectUser
|
||||
},
|
||||
setup() {
|
||||
const {t} = useI18n()
|
||||
const store:any = useStore()
|
||||
let filter:any = reactive({
|
||||
dataList:computed(()=>{
|
||||
return store.state.adminPage.allUserList
|
||||
}),
|
||||
})
|
||||
|
||||
let filterData:any = reactive({
|
||||
|
||||
@@ -32,27 +32,16 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Email:</span>
|
||||
<input
|
||||
<!-- <input
|
||||
v-model="email"
|
||||
placeholder="Please enter email"
|
||||
@keydown.enter="gettrialList"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User Name:</span>
|
||||
<a-select
|
||||
v-model:value="ids"
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Item..."
|
||||
max-tag-count="responsive"
|
||||
:options="allUserList"
|
||||
@keydown.enter="gettrialList"
|
||||
></a-select>
|
||||
/> -->
|
||||
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||
</div>
|
||||
|
||||
<div class="admin_state_item">
|
||||
<span>Organization Name:</span>
|
||||
<input
|
||||
@@ -95,8 +84,11 @@
|
||||
import { defineComponent, ref, createVNode, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { Https } from "@/tool/https";
|
||||
import SelectUser from "@/component/common/SelectUser.vue";
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
components: {
|
||||
SelectUser
|
||||
},
|
||||
setup() {
|
||||
const store: any = useStore();
|
||||
let rangePickerValue: any = ref([]);
|
||||
@@ -238,9 +230,6 @@
|
||||
];
|
||||
});
|
||||
|
||||
let allUserList: any = computed(() => {
|
||||
return store.state.adminPage.allUserList;
|
||||
});
|
||||
let ids = ref([]);
|
||||
let email = ref("");
|
||||
let dataList: any = ref([]);
|
||||
@@ -255,7 +244,6 @@
|
||||
rangeTimeValue,
|
||||
columns,
|
||||
dataList,
|
||||
allUserList,
|
||||
ids,
|
||||
email,
|
||||
renameData,
|
||||
@@ -312,7 +300,7 @@
|
||||
endTime: endDate,
|
||||
startTime: startDate,
|
||||
ids: ids,
|
||||
email: this.email.trim(),
|
||||
email: this.email?.trim(),
|
||||
organizationName: this.organizationName,
|
||||
};
|
||||
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Item..."
|
||||
max-tag-count="responsive"
|
||||
:options="countryList"
|
||||
:options="allCountry"
|
||||
></a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
@@ -192,9 +192,6 @@ export default defineComponent({
|
||||
cityList: computed(()=>{
|
||||
return store.state.adminPage.city
|
||||
}),
|
||||
countryList: computed(()=>{
|
||||
return store.state.adminPage.country
|
||||
}),
|
||||
isAwayOrUnfold:false,
|
||||
});
|
||||
let filterData: any = reactive({
|
||||
@@ -471,9 +468,10 @@ export default defineComponent({
|
||||
filter.dataList = rv.content;
|
||||
filterData.total = rv.total;
|
||||
filter.tableLoading = false;
|
||||
rv.content.forEach((item: any) => {
|
||||
filterData.totalPayer += Number(item.payerTotal)
|
||||
})
|
||||
filterData.totalPayer = rv.content.reduce((total: number, item: any) => {
|
||||
const value = item && item.status === 'Success' ? parseFloat(item.payerTotal) : 0;
|
||||
return total + (isNaN(value) ? 0 : value);
|
||||
}, 0);
|
||||
|
||||
// this.workspaceItem.position = this.singleTypeList[0].label
|
||||
}
|
||||
|
||||
@@ -139,9 +139,6 @@ export default defineComponent({
|
||||
let filter: any = reactive({
|
||||
dataList: [],
|
||||
tableLoading: false,
|
||||
allUserList: computed(()=>{
|
||||
return store.state.adminPage.allUserList
|
||||
}),
|
||||
rowSelection:computed(() => {
|
||||
return {
|
||||
selectedRowKeys: unref(selectedRowKeys),
|
||||
|
||||
@@ -40,27 +40,16 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Email:</span>
|
||||
<input
|
||||
<!-- <input
|
||||
v-model="email"
|
||||
placeholder="Please enter email"
|
||||
@keydown.enter="gettrialList"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User Name:</span>
|
||||
<a-select
|
||||
v-model:value="ids"
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Item..."
|
||||
max-tag-count="responsive"
|
||||
:options="allUserList"
|
||||
@keydown.enter="gettrialList"
|
||||
></a-select>
|
||||
/> -->
|
||||
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||
</div>
|
||||
|
||||
<div class="admin_state_item">
|
||||
<span>User Type:</span>
|
||||
<a-select
|
||||
@@ -160,16 +149,14 @@ import { formatTime } from "@/tool/util";
|
||||
import { useStore } from "vuex";
|
||||
import { Https } from "@/tool/https";
|
||||
import allUserPoerationsVue from "./allUserPoerations.vue";
|
||||
import SelectUser from '@/component/common/SelectUser.vue'
|
||||
export default defineComponent({
|
||||
components: {allUserPoerationsVue,},
|
||||
components: {allUserPoerationsVue,SelectUser},
|
||||
setup() {
|
||||
const store:any = useStore()
|
||||
let filter: any = reactive({
|
||||
dataList: [],
|
||||
tableLoading: false,
|
||||
allUserList: computed(()=>{
|
||||
return store.state.adminPage.allUserList
|
||||
}),
|
||||
allCountry:[],
|
||||
isAwayOrUnfold:false
|
||||
});
|
||||
@@ -436,7 +423,7 @@ export default defineComponent({
|
||||
page: filterData.currentPage,
|
||||
systemUser: filterData.systemUser,
|
||||
country: filterData.country,
|
||||
email: filterData.email.trim(),
|
||||
email: filterData.email?.trim(),
|
||||
userType: filterData.userType,
|
||||
ids: filterData.ids,
|
||||
occupation: filterData.occupation,
|
||||
|
||||
@@ -1,352 +1,392 @@
|
||||
<template>
|
||||
<div class="allUserPoerationModal" ref="allUserPoerationModal"></div>
|
||||
<a-modal
|
||||
class="allUserPoeration_modal generalModel"
|
||||
v-model:visible="operationsModal"
|
||||
:footer="null"
|
||||
:get-container="() => $refs.allUserPoerationModal"
|
||||
width="50%"
|
||||
:maskClosable="false"
|
||||
:centered="true"
|
||||
:closable="false"
|
||||
:mask="true"
|
||||
wrapClassName="#app"
|
||||
:keyboard="false"
|
||||
>
|
||||
<div class="generalModel_btn">
|
||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
||||
<svg width="100%" height="100%" viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3"/>
|
||||
<rect x="32.5063" y="12" width="3" height="29" rx="1.5" transform="rotate(45 32.5063 12)" fill="white"/>
|
||||
<rect x="34.6274" y="32.5059" width="3" height="29" rx="1.5" transform="rotate(135 34.6274 32.5059)" fill="white"/>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal_title_text">
|
||||
<div>{{ title }} User</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_center admin_page">
|
||||
<div class="admin_state_item">
|
||||
<span>User Name: <span>*</span></span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{active:title != 'Add'}"
|
||||
v-model="userName"
|
||||
placeholder="Please enter user name"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User Email: <span>*</span></span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{active:title != 'Add'}"
|
||||
v-model="userEmail"
|
||||
placeholder="Please enter email"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Create Time: <span>*</span></span>
|
||||
<a-date-picker :disabled="title != 'Add'" style="width: 250px" valueFormat="YYYY-MM-DDTHH:mm:ss" class="range_picker" show-time placeholder="Create Time" v-model:value="validStartTime">
|
||||
<template #suffixIcon>
|
||||
<span
|
||||
class="icon iconfont range_picker_icon icon-rili"
|
||||
></span>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>End Time: <span>*</span></span>
|
||||
<a-date-picker style="width: 250px" valueFormat="YYYY-MM-DDTHH:mm:ss" class="range_picker" show-time placeholder="End Time" v-model:value="validEndTime">
|
||||
<template #suffixIcon>
|
||||
<span
|
||||
class="icon iconfont range_picker_icon icon-rili"
|
||||
></span>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User Type:<span>*</span></span>
|
||||
<a-select
|
||||
v-model:value="systemUser"
|
||||
size="large"
|
||||
style="width: 250px"
|
||||
optionFilterProp="label"
|
||||
:options="state"
|
||||
placeholder="Please select"
|
||||
allowClear
|
||||
show-search
|
||||
></a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Credits:</span>
|
||||
<input
|
||||
v-model="credits"
|
||||
placeholder="Please enter credits"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Country or Region:</span>
|
||||
<input
|
||||
<div class="allUserPoerationModal" ref="allUserPoerationModal"></div>
|
||||
<a-modal
|
||||
class="allUserPoeration_modal generalModel"
|
||||
v-model:visible="operationsModal"
|
||||
:footer="null"
|
||||
:get-container="() => $refs.allUserPoerationModal"
|
||||
width="50%"
|
||||
:maskClosable="false"
|
||||
:centered="true"
|
||||
:closable="false"
|
||||
:mask="true"
|
||||
wrapClassName="#app"
|
||||
:keyboard="false"
|
||||
>
|
||||
<div class="generalModel_btn">
|
||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 46 46"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3" />
|
||||
<rect
|
||||
x="32.5063"
|
||||
y="12"
|
||||
width="3"
|
||||
height="29"
|
||||
rx="1.5"
|
||||
transform="rotate(45 32.5063 12)"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="34.6274"
|
||||
y="32.5059"
|
||||
width="3"
|
||||
height="29"
|
||||
rx="1.5"
|
||||
transform="rotate(135 34.6274 32.5059)"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal_title_text">
|
||||
<div>{{ title }} User</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_center admin_page">
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
User Name:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{ active: title != 'Add' }"
|
||||
v-model="userName"
|
||||
placeholder="Please enter user name"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
User Email:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{ active: title != 'Add' }"
|
||||
v-model="userEmail"
|
||||
placeholder="Please enter email"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
Create Time:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-date-picker
|
||||
:disabled="title != 'Add'"
|
||||
style="width: 250px"
|
||||
valueFormat="YYYY-MM-DDTHH:mm:ss"
|
||||
class="range_picker"
|
||||
show-time
|
||||
placeholder="Create Time"
|
||||
v-model:value="validStartTime"
|
||||
>
|
||||
<template #suffixIcon>
|
||||
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
End Time:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-date-picker
|
||||
style="width: 250px"
|
||||
valueFormat="YYYY-MM-DDTHH:mm:ss"
|
||||
class="range_picker"
|
||||
show-time
|
||||
placeholder="End Time"
|
||||
v-model:value="validEndTime"
|
||||
>
|
||||
<template #suffixIcon>
|
||||
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
||||
</template>
|
||||
</a-date-picker>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>
|
||||
User Type:
|
||||
<span>*</span>
|
||||
</span>
|
||||
<a-select
|
||||
v-model:value="systemUser"
|
||||
size="large"
|
||||
style="width: 250px"
|
||||
optionFilterProp="label"
|
||||
:options="state"
|
||||
placeholder="Please select"
|
||||
allowClear
|
||||
show-search
|
||||
></a-select>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Credits:</span>
|
||||
<input
|
||||
v-model="credits"
|
||||
placeholder="Please enter credits"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Country or Region:</span>
|
||||
<!-- <input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{active:title != 'Add'}"
|
||||
v-model="country"
|
||||
placeholder="Please enter country"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Organization Name:</span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{active:title != 'Add'}"
|
||||
v-model="organizationName"
|
||||
placeholder="Please enter Organization Name"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Sub Account Num:</span>
|
||||
<input
|
||||
:disabled="title != 'Add'"
|
||||
:class="{active:title != 'Add'}"
|
||||
v-model="subAccountNum"
|
||||
placeholder="Please enter Sub Account Num"
|
||||
type="number"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_btn admin_page">
|
||||
<div class="admin_search_item" @click="cancelDsign">
|
||||
Close
|
||||
</div>
|
||||
<div class="admin_search_item" @click="setOk">
|
||||
OK
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
<div class="mark_loading" v-show="loadingShow">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
/> -->
|
||||
<a-select
|
||||
v-model:value="country"
|
||||
:disabled="title != 'Add'"
|
||||
:class="{ active: title != 'Add' }"
|
||||
:allowClear="true"
|
||||
show-search
|
||||
style="width: 250px"
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Country or Region"
|
||||
max-tag-count="responsive"
|
||||
:options="allCountry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="allUserPoeration_btn admin_page">
|
||||
<div class="admin_search_item" @click="cancelDsign">Close</div>
|
||||
<div class="admin_search_item" @click="setOk">OK</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
<div class="mark_loading" v-show="loadingShow">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { defineComponent, ref, reactive, watch, onMounted, nextTick, toRefs } from "vue";
|
||||
import { Https } from "@/tool/https";
|
||||
import { Modal, message } from "ant-design-vue";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
||||
import { formatTime } from "@/tool/util";
|
||||
import { defineComponent, ref, reactive, watch, onMounted, nextTick, toRefs } from 'vue'
|
||||
import { Https } from '@/tool/https'
|
||||
import { Modal, message } from 'ant-design-vue'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { formatTime } from '@/tool/util'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
},
|
||||
emits: ['searchHistoryList'],
|
||||
setup(props,{emit}) {
|
||||
let operations = reactive({
|
||||
operationsModal:false,
|
||||
operationsEdit:false,
|
||||
loadingShow:false,
|
||||
title:''
|
||||
})
|
||||
let operationsData = reactive({
|
||||
accountId:-1,
|
||||
userName:'',
|
||||
userEmail:'',
|
||||
validStartTime:'',
|
||||
validEndTime:'',
|
||||
systemUser:'',
|
||||
credits:'',
|
||||
country:'',
|
||||
organizationName:'',
|
||||
subAccountNum:0,
|
||||
})
|
||||
let state = ref([
|
||||
{
|
||||
label:'visitor',
|
||||
value:'0',
|
||||
},
|
||||
{
|
||||
label:'yearly',
|
||||
value:'1',
|
||||
},
|
||||
{
|
||||
label:'monthly',
|
||||
value:'2',
|
||||
},
|
||||
{
|
||||
label:'trial',
|
||||
value:'3',
|
||||
},
|
||||
{
|
||||
label: "userInEvent",
|
||||
value: "4",
|
||||
},
|
||||
{
|
||||
label: "Edu Admin",
|
||||
value: "7",
|
||||
},
|
||||
]);
|
||||
let init = (funStr,data)=>{
|
||||
operations.operationsModal = true
|
||||
operations.operationsEdit = true
|
||||
operations.title = funStr
|
||||
if(funStr == 'Add') operations.operationsEdit = false
|
||||
if(funStr == 'Edit'){
|
||||
operationsData.organizationName = data.organizationName
|
||||
operationsData.subAccountNum = data.subAccountNum?data.subAccountNum:0
|
||||
let startTime = data.validStartTime?formatTime(data.validStartTime / 1000,"YYYY-MM-DDThh:mm:ss"):''
|
||||
let endTime = data.validEndTime?formatTime(data.validEndTime / 1000,"YYYY-MM-DDThh:mm:ss"):''
|
||||
operationsData.accountId=data.id
|
||||
operationsData.userName=data.userName
|
||||
operationsData.userEmail=data.userEmail
|
||||
// operationsData.validStartTime='2024-08-05T00:00:06'
|
||||
// operationsData.validEndTime='2024-08-05T00:00:06'
|
||||
operationsData.validStartTime=startTime
|
||||
operationsData.validEndTime=endTime
|
||||
operationsData.systemUser=String(data.systemUser)
|
||||
operationsData.credits=data.credits
|
||||
operationsData.country=data.country
|
||||
// operationsData.accountId = data.accountId
|
||||
// operationsData.userName = data.userName
|
||||
// operationsData.userEmail = data.userEmail
|
||||
// operationsData.validStartTime = formatTime(data.validStartTime)
|
||||
// operationsData.validEndTime = formatTime(data.validEndTime)
|
||||
}
|
||||
components: {},
|
||||
emits: ['searchHistoryList'],
|
||||
setup(props, { emit }) {
|
||||
let operations = reactive({
|
||||
operationsModal: false,
|
||||
operationsEdit: false,
|
||||
loadingShow: false,
|
||||
title: ''
|
||||
})
|
||||
let operationsData = reactive({
|
||||
accountId: -1,
|
||||
userName: '',
|
||||
userEmail: '',
|
||||
validStartTime: '',
|
||||
validEndTime: '',
|
||||
systemUser: '',
|
||||
credits: '',
|
||||
country: ''
|
||||
})
|
||||
let state = ref([
|
||||
{
|
||||
label: 'visitor',
|
||||
value: '0'
|
||||
},
|
||||
{
|
||||
label: 'yearly',
|
||||
value: '1'
|
||||
},
|
||||
{
|
||||
label: 'monthly',
|
||||
value: '2'
|
||||
},
|
||||
{
|
||||
label: 'trial',
|
||||
value: '3'
|
||||
},
|
||||
{
|
||||
label: 'userInEvent',
|
||||
value: '4'
|
||||
},
|
||||
{
|
||||
label: 'Edu Admin',
|
||||
value: '7'
|
||||
}
|
||||
])
|
||||
let init = (funStr, data) => {
|
||||
operations.operationsModal = true
|
||||
operations.operationsEdit = true
|
||||
operations.title = funStr
|
||||
if (funStr == 'Add') operations.operationsEdit = false
|
||||
if (funStr == 'Edit') {
|
||||
operationsData.organizationName = data.organizationName
|
||||
operationsData.subAccountNum = data.subAccountNum ? data.subAccountNum : 0
|
||||
let startTime = data.validStartTime
|
||||
? formatTime(data.validStartTime / 1000, 'YYYY-MM-DDThh:mm:ss')
|
||||
: ''
|
||||
let endTime = data.validEndTime
|
||||
? formatTime(data.validEndTime / 1000, 'YYYY-MM-DDThh:mm:ss')
|
||||
: ''
|
||||
operationsData.accountId = data.id
|
||||
operationsData.userName = data.userName
|
||||
operationsData.userEmail = data.userEmail
|
||||
// operationsData.validStartTime='2024-08-05T00:00:06'
|
||||
// operationsData.validEndTime='2024-08-05T00:00:06'
|
||||
operationsData.validStartTime = startTime
|
||||
operationsData.validEndTime = endTime
|
||||
operationsData.systemUser = String(data.systemUser)
|
||||
operationsData.credits = data.credits
|
||||
operationsData.country = data.country
|
||||
// operationsData.accountId = data.accountId
|
||||
// operationsData.userName = data.userName
|
||||
// operationsData.userEmail = data.userEmail
|
||||
// operationsData.validStartTime = formatTime(data.validStartTime)
|
||||
// operationsData.validEndTime = formatTime(data.validEndTime)
|
||||
}
|
||||
}
|
||||
let setTime = time => {
|
||||
if (time) {
|
||||
const date = new Date(time)
|
||||
const timestamp = date.getTime() // 转换为秒数
|
||||
return timestamp
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
let setAddData = () => {
|
||||
return {
|
||||
country: operationsData.country,
|
||||
credits: operationsData.credits,
|
||||
systemUser: operationsData.systemUser,
|
||||
userEmail: operationsData.userEmail,
|
||||
userName: operationsData.userName,
|
||||
validEndTime: setTime(operationsData.validEndTime),
|
||||
validStartTime: setTime(operationsData.validStartTime),
|
||||
organizationName: operationsData.organizationName
|
||||
? operationsData.organizationName
|
||||
: null,
|
||||
subAccountNum: operationsData.subAccountNum
|
||||
}
|
||||
}
|
||||
let setEditData = () => {
|
||||
return {
|
||||
accountId: operationsData.accountId,
|
||||
credits: operationsData.credits,
|
||||
systemUser: operationsData.systemUser,
|
||||
validEndTime: setTime(operationsData.validEndTime),
|
||||
userName: operationsData.userName,
|
||||
userEmail: operationsData.userEmail
|
||||
}
|
||||
}
|
||||
let cancelDsign = () => {
|
||||
operationsData.accountId = -1
|
||||
operationsData.userName = ''
|
||||
operationsData.userEmail = ''
|
||||
operationsData.validStartTime = ''
|
||||
operationsData.validEndTime = ''
|
||||
operationsData.systemUser = ''
|
||||
operationsData.credits = ''
|
||||
operationsData.country = ''
|
||||
operations.operationsModal = false
|
||||
}
|
||||
let setOk = () => {
|
||||
let data
|
||||
if (operations.title == 'Add') {
|
||||
data = setAddData()
|
||||
if (
|
||||
!data.userName ||
|
||||
!data.userEmail ||
|
||||
!data.validStartTime ||
|
||||
!data.validEndTime ||
|
||||
!data.systemUser
|
||||
)
|
||||
return message.warning('Please check the input box marked with *')
|
||||
Https.axiosPost(Https.httpUrls.adminAddUser, data).then(rv => {
|
||||
if (rv) {
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
data = setEditData()
|
||||
if (!data.userName || !data.userEmail || !data.validEndTime || !data.systemUser)
|
||||
return message.warning('Please check the input box marked with *')
|
||||
Https.axiosPost(Https.httpUrls.modifyUser, {}, { params: data }).then(rv => {
|
||||
if (rv) {
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
let setTime = (time) =>{
|
||||
if(time){
|
||||
const date = new Date(time);
|
||||
const timestamp = date.getTime(); // 转换为秒数
|
||||
return timestamp
|
||||
}else{
|
||||
return ''
|
||||
}
|
||||
|
||||
}
|
||||
let setAddData = ()=>{
|
||||
return {
|
||||
"country": operationsData.country,
|
||||
"credits": operationsData.credits,
|
||||
"systemUser": operationsData.systemUser,
|
||||
"userEmail": operationsData.userEmail,
|
||||
"userName": operationsData.userName,
|
||||
"validEndTime": setTime(operationsData.validEndTime),
|
||||
"validStartTime": setTime(operationsData.validStartTime),
|
||||
"organizationName": operationsData.organizationName?operationsData.organizationName:null,
|
||||
"subAccountNum": operationsData.subAccountNum,
|
||||
}
|
||||
}
|
||||
let setEditData = ()=>{
|
||||
return {
|
||||
"accountId": operationsData.accountId,
|
||||
"credits": operationsData.credits,
|
||||
"systemUser": operationsData.systemUser,
|
||||
"validEndTime": setTime(operationsData.validEndTime),
|
||||
"userName": operationsData.userName,
|
||||
"userEmail": operationsData.userEmail,
|
||||
}
|
||||
}
|
||||
let cancelDsign = ()=>{
|
||||
operationsData.accountId=-1
|
||||
operationsData.userName=''
|
||||
operationsData.userEmail=''
|
||||
operationsData.validStartTime=''
|
||||
operationsData.validEndTime=''
|
||||
operationsData.systemUser=''
|
||||
operationsData.credits=''
|
||||
operationsData.country=''
|
||||
operations.operationsModal = false
|
||||
}
|
||||
let setOk = ()=>{
|
||||
let data
|
||||
if(operations.title == 'Add'){
|
||||
data = setAddData()
|
||||
if(!data.userName || !data.userEmail || !data.validStartTime || !data.validEndTime || !data.systemUser)return message.warning('Please check the input box marked with *')
|
||||
Https.axiosPost(Https.httpUrls.adminAddUser, data).then(
|
||||
(rv) => {
|
||||
if (rv) {
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
}
|
||||
);
|
||||
}else{
|
||||
data = setEditData()
|
||||
if(!data.userName || !data.userEmail || !data.validEndTime || !data.systemUser)return message.warning('Please check the input box marked with *')
|
||||
Https.axiosPost(Https.httpUrls.modifyUser,{},{params:data}).then(
|
||||
(rv) => {
|
||||
if (rv) {
|
||||
cancelDsign()
|
||||
emit('searchHistoryList')
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
...toRefs(operations),
|
||||
...toRefs(operationsData),
|
||||
state,
|
||||
cancelDsign,
|
||||
init,
|
||||
setOk,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
|
||||
},
|
||||
});
|
||||
const allCountry = ref([])
|
||||
const filterOption = (input, option) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
onMounted(() => {
|
||||
const countryList = sessionStorage.getItem('allCountry')
|
||||
if (countryList) {
|
||||
allCountry.value = JSON.parse(countryList)
|
||||
}
|
||||
})
|
||||
return {
|
||||
...toRefs(operations),
|
||||
...toRefs(operationsData),
|
||||
state,
|
||||
cancelDsign,
|
||||
init,
|
||||
setOk,
|
||||
allCountry,
|
||||
filterOption
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
:deep(.allUserPoeration_modal){
|
||||
.ant-modal-body{
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
:deep(.allUserPoeration_modal) {
|
||||
.ant-modal-body {
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
|
||||
.allUserPoeration_modal {
|
||||
.closeIcon {
|
||||
z-index: 2;
|
||||
.closeIcon {
|
||||
z-index: 2;
|
||||
}
|
||||
> .admin_state_item {
|
||||
> span {
|
||||
width: 15rem;
|
||||
}
|
||||
> .admin_state_item{
|
||||
> span{
|
||||
width: 15rem;
|
||||
}
|
||||
}
|
||||
.allUserPoeration_btn{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
justify-content: flex-end;
|
||||
padding: 1rem 0;
|
||||
.admin_search_item{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.allUserPoeration_center{
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.allUserPoeration_btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
justify-content: flex-end;
|
||||
padding: 1rem 0;
|
||||
.admin_search_item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.allUserPoeration_center {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -25,15 +25,7 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User:</span>
|
||||
<a-select
|
||||
v-model:value="userIdList"
|
||||
mode="multiple"
|
||||
style="width: 280px"
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Item..."
|
||||
max-tag-count="responsive"
|
||||
:options="dataList"
|
||||
></a-select>
|
||||
<SelectUser v-model="userIdList" labelKey="email" multiple />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -62,15 +54,14 @@ import { PieChart } from 'echarts/charts';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { useStore } from "vuex";
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import SelectUser from '@/component/common/SelectUser.vue';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectUser
|
||||
},
|
||||
setup() {
|
||||
const store:any = useStore()
|
||||
let filter:any = reactive({
|
||||
dataList:computed(()=>{
|
||||
return store.state.adminPage.allUserList
|
||||
}),
|
||||
})
|
||||
|
||||
let filterData:any = reactive({
|
||||
|
||||
1133
src/component/Administrator/subscriptionPlan.vue
Normal file
@@ -37,26 +37,7 @@
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>Email:</span>
|
||||
<input
|
||||
v-model="email"
|
||||
placeholder="Please enter email"
|
||||
@keydown.enter="gettrialList"
|
||||
type="text"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</div>
|
||||
<div class="admin_state_item">
|
||||
<span>User Name:</span>
|
||||
<a-select
|
||||
v-model:value="ids"
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
:filter-option="filterOption"
|
||||
placeholder="Select Item..."
|
||||
max-tag-count="responsive"
|
||||
:options="allUserList"
|
||||
@keydown.enter="gettrialList"
|
||||
></a-select>
|
||||
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin_search">
|
||||
@@ -96,17 +77,17 @@ import { defineComponent, ref, createVNode, computed, reactive, toRefs, onMounte
|
||||
import { formatTime } from "@/tool/util";
|
||||
import { useStore } from "vuex";
|
||||
import { Https } from "@/tool/https";
|
||||
import SelectUser from '@/component/common/SelectUser.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectUser
|
||||
},
|
||||
setup() {
|
||||
const store:any = useStore()
|
||||
let filter:any = reactive({
|
||||
dataList:[],
|
||||
tableLoading:false,
|
||||
allUserList: computed(()=>{
|
||||
return store.state.adminPage.allUserList
|
||||
}),
|
||||
|
||||
allCountry:[]
|
||||
})
|
||||
let filterData:any = reactive({
|
||||
|
||||
@@ -69,7 +69,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
||||
layer.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||
this.newFill = new fabric.Rect({
|
||||
width: clippingMaskFabricObject.width,
|
||||
@@ -117,7 +117,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
||||
this.parent.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||
this.newFill = new fabric.Rect({
|
||||
width: clippingMaskFabricObject.width,
|
||||
@@ -222,7 +222,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
||||
this.parent?.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clipPath.clipPath = null;
|
||||
// clipPath.clipPath = null;
|
||||
clipPath.set({ absolutePositioned: true });
|
||||
this.group.clipPath = clipPath;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
|
||||
layer.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
|
||||
clippingMaskFabricObject.set({
|
||||
// 设置绝对定位
|
||||
|
||||
328
src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
Normal file
@@ -0,0 +1,328 @@
|
||||
import { Command } from "./Command";
|
||||
import { findLayerRecursively } from "../utils/layerHelper";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
findObjectById,
|
||||
generateId,
|
||||
insertObjectAtZIndex,
|
||||
removeCanvasObjectByObject,
|
||||
createPatternTransform,
|
||||
imageAddGapToCanvas,
|
||||
} from "../utils/helper";
|
||||
import { restoreFabricObject } from "../utils/objectHelper";
|
||||
|
||||
const scale = 0.3;// 默认缩放比例
|
||||
|
||||
export const FillSourceToBase64 = (source) => {
|
||||
if (source?.toDataURL) {
|
||||
return source.toDataURL?.();
|
||||
} else if (source?.src) {
|
||||
return source.src;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充图案平铺命令
|
||||
* 填充重复属性:repeat | repeat-x | repeat-y | no-repeat
|
||||
* 默认缩放比例:0.3
|
||||
* 默认偏移量:50%
|
||||
*/
|
||||
export class FillRepeatCommand extends Command {
|
||||
constructor(options) {
|
||||
super({ name: "填充图案平铺", saveState: true });
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.fillRepeat = options.fillRepeat;
|
||||
this.oldObjects = null;
|
||||
this.oldLocked = null;
|
||||
this.oldIsDisableUnlock = null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.warn("图层不存在或没有 fabric 对象");
|
||||
return false;
|
||||
}
|
||||
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||
if (!object || (object.type !== "rect" && object.type !== "image")) {
|
||||
console.warn("当前对象不能平铺", object.type);
|
||||
return false;
|
||||
}
|
||||
this.oldObjects = object;
|
||||
if (this.fillRepeat === "no-repeat") {
|
||||
const fill_ = object.fill_;
|
||||
const image = await new Promise((resolve, reject) => {
|
||||
fabric.Image.fromURL(
|
||||
fill_.source,
|
||||
v => resolve(v),
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
});
|
||||
image.set({
|
||||
id: object.id,
|
||||
layerId: object.layerId,
|
||||
layerName: object.layerName,
|
||||
...(fill_.originalInfo || {
|
||||
top: object.top,
|
||||
left: object.left,
|
||||
})
|
||||
});
|
||||
layer.fabricObjects = [image.toObject(["id", "layerId", "layerName"])];
|
||||
this.oldLocked = layer.locked;
|
||||
layer.locked = false;
|
||||
|
||||
this.canvas.add(image);
|
||||
this.canvas.remove(object);
|
||||
} else {
|
||||
const img = await new Promise((resolve, reject) => {
|
||||
if (object.type === "rect") {
|
||||
let source = object.fill.source;
|
||||
resolve(source);
|
||||
} else if (object.type === "image") {
|
||||
const imgElement = object.getElement();
|
||||
// 创建透明 Canvas
|
||||
const tcanvas = document.createElement('canvas');
|
||||
tcanvas.width = imgElement.width;
|
||||
tcanvas.height = imgElement.height;
|
||||
const ctx = tcanvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||
ctx.drawImage(imgElement, 0, 0);
|
||||
resolve(tcanvas);
|
||||
}
|
||||
});
|
||||
const fill_ = object.fill_ || {
|
||||
source: FillSourceToBase64(img),
|
||||
gapX: 0,
|
||||
gapY: 0,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
originalInfo: {
|
||||
top: object.top,
|
||||
left: object.left,
|
||||
scaleX: object.scaleX,
|
||||
scaleY: object.scaleY,
|
||||
width: object.width,
|
||||
height: object.height,
|
||||
}
|
||||
};
|
||||
const bgObject = this.canvasManager.getBackgroundLayerObject();
|
||||
const pattern = new fabric.Pattern({
|
||||
source: img,
|
||||
repeat: this.fillRepeat,
|
||||
patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
|
||||
offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
|
||||
offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
|
||||
});
|
||||
const rect = new fabric.Rect({
|
||||
id: object.id,
|
||||
layerId: object.layerId,
|
||||
layerName: object.layerName,
|
||||
fill_,
|
||||
});
|
||||
layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
|
||||
this.oldLocked = layer.locked;
|
||||
// this.oldIsDisableUnlock = layer.isDisableUnlock;
|
||||
// layer.isDisableUnlock = true;
|
||||
if (this.oldObjects.type === "rect") {
|
||||
rect.set({
|
||||
width: object.width,
|
||||
height: object.height,
|
||||
top: object.top,
|
||||
left: object.left,
|
||||
originX: object.originX,
|
||||
originY: object.originY,
|
||||
angle: object.angle,
|
||||
scaleX: object.scaleX,
|
||||
scaleY: object.scaleY,
|
||||
flipX: object.flipX,
|
||||
flipY: object.flipY,
|
||||
});
|
||||
} else {
|
||||
let scaleX = bgObject.scaleX || 1;
|
||||
let scaleY = bgObject.scaleY || 1;
|
||||
rect.set({
|
||||
width: bgObject.width,
|
||||
height: bgObject.height,
|
||||
top: bgObject.top - bgObject.height * scaleY / 2,
|
||||
left: bgObject.left - bgObject.width * scaleX / 2,
|
||||
scaleX,
|
||||
scaleY,
|
||||
});
|
||||
layer.locked = true;
|
||||
}
|
||||
rect.set("fill", pattern);
|
||||
this.canvas.add(rect);
|
||||
this.canvas.remove(object);
|
||||
}
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
await this.layerManager?.sortLayersWithTool?.();
|
||||
await this.canvasManager.thumbnailManager?.generateLayerThumbnail(
|
||||
this.layerId
|
||||
);
|
||||
await this.layerManager.selectLayerObjects(this.layerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.oldObjects) {
|
||||
console.warn("没有旧对象可恢复");
|
||||
return false;
|
||||
}
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.oldObjects.layerId);
|
||||
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.warn("图层不存在或没有 fabric 对象");
|
||||
return false;
|
||||
}
|
||||
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||
this.canvas.remove(object);
|
||||
this.canvas.add(this.oldObjects);
|
||||
layer.fabricObjects = [this.oldObjects.toObject(["id", "layerId", "layerName"])];
|
||||
layer.locked = this.oldLocked;
|
||||
// layer.isDisableUnlock = this.oldIsDisableUnlock;
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
await this.layerManager?.sortLayersWithTool?.();
|
||||
this.canvas.renderAll();
|
||||
this.canvasManager.thumbnailManager?.generateLayerThumbnail(this.layerId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 填充图案更改参数
|
||||
*/
|
||||
export class FillRepeatChangeCommand extends Command {
|
||||
constructor(options) {
|
||||
super({ name: "填充图案更改参数", saveState: true });
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.newPattern = options.newPattern;
|
||||
this.oldPattern = null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.warn("图层不存在或没有 fabric 对象");
|
||||
return false;
|
||||
}
|
||||
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||
if (!object || object.type !== "rect") {
|
||||
console.warn("当前对象不是矩形", object);
|
||||
return false;
|
||||
}
|
||||
this.oldPattern = object.oldPattern || object.get("fill");
|
||||
delete object.oldPattern;
|
||||
const oldPattern = { ...this.oldPattern };
|
||||
delete oldPattern.id;
|
||||
const pattern = new fabric.Pattern({
|
||||
...oldPattern,
|
||||
...this.newPattern,
|
||||
});
|
||||
object.set("fill", pattern);
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (!this.oldPattern) {
|
||||
console.warn("没有旧图案可恢复");
|
||||
return false;
|
||||
}
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.warn("图层不存在或没有 fabric 对象");
|
||||
return false;
|
||||
}
|
||||
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||
if (!object || object.type !== "rect") {
|
||||
console.warn("当前对象不是矩形", object);
|
||||
return false;
|
||||
}
|
||||
const pattern = new fabric.Pattern({
|
||||
...this.oldPattern
|
||||
});
|
||||
object.set("fill", pattern);
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充图案更改间隙
|
||||
*/
|
||||
export class FillRepeatGapChangeCommand extends Command {
|
||||
constructor(options) {
|
||||
super({ name: "填充图案更改间隙", saveState: true });
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.newGapX = options.newGapX;
|
||||
this.newGapY = options.newGapY;
|
||||
this.record = !!options.record;
|
||||
this.oldGapX = null;
|
||||
this.oldGapY = null;
|
||||
}
|
||||
|
||||
async execute(isUndo = false) {
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.warn("图层不存在或没有 fabric 对象");
|
||||
return false;
|
||||
}
|
||||
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
|
||||
if (!object || object.type !== "rect") {
|
||||
console.warn("当前对象不是矩形", object);
|
||||
return false;
|
||||
}
|
||||
if (!object.fill_) {
|
||||
object.fill_ = {
|
||||
source: FillSourceToBase64(object.fill.source),
|
||||
gapX: 0,
|
||||
gapY: 0,
|
||||
}
|
||||
}
|
||||
if (isUndo) {
|
||||
object.fill_.gapX = this.oldGapX;
|
||||
object.fill_.gapY = this.oldGapY;
|
||||
} else {
|
||||
if (!object.oldFill_ && this.record) {
|
||||
object.oldFill_ = { ...object.fill_ };
|
||||
}
|
||||
this.oldGapX = object.fill_.gapX;
|
||||
this.oldGapY = object.fill_.gapY;
|
||||
object.fill_.gapX = this.newGapX;
|
||||
object.fill_.gapY = this.newGapY;
|
||||
}
|
||||
const image = new Image();
|
||||
image.src = object.fill_.source;
|
||||
await image.decode();
|
||||
object.fill_.width = image.width;
|
||||
object.fill_.height = image.height;
|
||||
const fill = object.get("fill");
|
||||
fill.source = imageAddGapToCanvas(image, object.fill_.gapX, object.fill_.gapY);
|
||||
object.set("fill", new fabric.Pattern(fill));
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
async undo() {
|
||||
if (this.oldGapX === null || this.oldGapY === null) {
|
||||
console.warn("没有旧间隙可恢复");
|
||||
return false;
|
||||
}
|
||||
await this.execute(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
|
||||
import { ToolCommand } from "./ToolCommands";
|
||||
import {
|
||||
findObjectById,
|
||||
findObjectByLayerId,
|
||||
generateId,
|
||||
getObjectZIndex,
|
||||
insertObjectAtZIndex,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
} from "../utils/helper";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { restoreFabricObject } from "../utils/objectHelper";
|
||||
|
||||
import EventManager from "../utils/event.js";
|
||||
/**
|
||||
* 添加图层命令
|
||||
*/
|
||||
@@ -36,7 +37,7 @@ export class AddLayerCommand extends Command {
|
||||
|
||||
this.insertIndex = options.insertIndex;
|
||||
this.oldActiveLayerId = null;
|
||||
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
|
||||
this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
|
||||
|
||||
this.options = options.options || {};
|
||||
}
|
||||
@@ -70,7 +71,7 @@ export class AddLayerCommand extends Command {
|
||||
|
||||
undo() {
|
||||
// 从图层列表删除该图层
|
||||
this.layers.value = [...this.beforeLayers];
|
||||
this.layers.value = JSON.parse(this.beforeLayers);
|
||||
|
||||
// 恢复原活动图层
|
||||
this.activeLayerId.value = this.oldActiveLayerId;
|
||||
@@ -143,7 +144,7 @@ export class AddLayerCommand extends Command {
|
||||
// 先在一级图层中查找
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
|
||||
if (layer.isPrintTrimsGroup) continue;
|
||||
if (layer.id === layerId) {
|
||||
return {
|
||||
layer: layer,
|
||||
@@ -251,12 +252,12 @@ export class PasteLayerCommand extends Command {
|
||||
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
|
||||
null;
|
||||
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
clippingMaskFabricObject.dirty = true;
|
||||
// clippingMaskFabricObject.dirty = true;
|
||||
clippingMaskFabricObject.setCoords();
|
||||
// 添加所有对象到画布
|
||||
allObjects.forEach((obj) => {
|
||||
@@ -523,6 +524,7 @@ export class RemoveLayerCommand extends Command {
|
||||
this.layerId = options.layerId;
|
||||
this.activeLayerId = options.activeLayerId;
|
||||
this.layerManager = options.layerManager || null;
|
||||
this.IsOnlyLayer = this.layers.value.filter((v => !v.isFixed && !v.isFixedOther && !v.isBackground)).length <= 1
|
||||
|
||||
// 查找要删除的图层
|
||||
this.layerIndex = this.layers.value.findIndex(
|
||||
@@ -599,7 +601,9 @@ export class RemoveLayerCommand extends Command {
|
||||
);
|
||||
// 从图层列表中删除
|
||||
this.layers.value.splice(this.layerIndex, 1);
|
||||
|
||||
if(this.IsOnlyLayer){
|
||||
this.addCmd = await this.layerManager?.createLayer?.(null, LayerType.EMPTY, {}, false);
|
||||
}
|
||||
// 如果删除的是当前活动图层,需要更新活动图层
|
||||
if (this.isActiveLayer) {
|
||||
// 查找最近的非背景层作为新的活动图层
|
||||
@@ -632,6 +636,9 @@ export class RemoveLayerCommand extends Command {
|
||||
async undo() {
|
||||
// 恢复图层到原位置
|
||||
if (this.layerIndex !== -1 && this.removedLayer) {
|
||||
if(this.IsOnlyLayer && this.addCmd){
|
||||
this.addCmd?.undo?.();
|
||||
}
|
||||
this.layers.value.splice(this.layerIndex, 0, this.removedLayer);
|
||||
|
||||
// 使用优化渲染批处理恢复真实对象到画布
|
||||
@@ -649,7 +656,6 @@ export class RemoveLayerCommand extends Command {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
this.canvas.renderAll();
|
||||
|
||||
@@ -802,15 +808,23 @@ export class ToggleLayerVisibilityCommand extends Command {
|
||||
|
||||
// 切换可见性
|
||||
this.layer.visible = !this.layer.visible;
|
||||
const ids = [this.layerId];
|
||||
const childLayers = this.layer?.children || [];
|
||||
childLayers.forEach((childLayer) => {
|
||||
childLayer.visible = this.layer.visible;
|
||||
ids.push(childLayer.id);
|
||||
});
|
||||
|
||||
// 更新画布上图层对象的可见性
|
||||
if (this.canvas) {
|
||||
const layerObjects = this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === this.layerId);
|
||||
layerObjects.forEach((obj) => {
|
||||
obj.visible = this.layer.visible;
|
||||
});
|
||||
this.canvas.getObjects().forEach((obj) => {
|
||||
if (ids.includes(obj.layerId)) {
|
||||
obj.getObjects?.()?.forEach((item) => {
|
||||
item.visible = this.layer.visible;
|
||||
});
|
||||
obj.visible = this.layer.visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
// 更新画布上对象的可选择状态
|
||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||
@@ -858,23 +872,24 @@ export class ToggleChildLayerVisibilityCommand extends Command {
|
||||
// this.oldVisibility = this.childLayer ? this.childLayer.visible : null;
|
||||
}
|
||||
|
||||
async execute() {
|
||||
async execute(visible) {
|
||||
if (!this.childLayer) {
|
||||
throw new Error("找不到要切换可见性的子图层");
|
||||
}
|
||||
|
||||
// 切换可见性
|
||||
this.childLayer.visible = !this.childLayer.visible;
|
||||
this.childLayer.visible = typeof visible === "boolean" ? visible : !this.childLayer.visible;
|
||||
|
||||
// 更新画布上图层对象的可见性
|
||||
if (this.canvas) {
|
||||
const layerObjects = this.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === this.layerId);
|
||||
|
||||
layerObjects.forEach((obj) => {
|
||||
obj.visible = this.childLayer.visible;
|
||||
});
|
||||
this.canvas.getObjects().forEach((obj) => {
|
||||
if (obj.layerId === this.layerId) {
|
||||
obj.getObjects?.()?.forEach((item) => {
|
||||
item.visible = this.childLayer.visible;
|
||||
});
|
||||
obj.visible = this.childLayer.visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新画布上对象的可选择状态
|
||||
@@ -1007,9 +1022,8 @@ export class LayerLockCommand extends Command {
|
||||
|
||||
// 如果是组图层,递归更新所有子图层
|
||||
if (
|
||||
layer.type === "group" &&
|
||||
layer.children &&
|
||||
Array.isArray(layer.children)
|
||||
Array.isArray(layer.children) && layer.children.length > 0
|
||||
) {
|
||||
layer.children.forEach((child) => {
|
||||
this._updateLayerLockState(child, locked);
|
||||
@@ -1108,7 +1122,7 @@ export class SetLayerOpacityCommand extends Command {
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1130,6 +1144,7 @@ export class SetLayerOpacityCommand extends Command {
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
@@ -1371,7 +1386,7 @@ export class GroupLayersCommand extends Command {
|
||||
// 备份原图层
|
||||
this.originalLayers = [...this.layers.value];
|
||||
// 新组ID
|
||||
this.groupId =
|
||||
this.groupId = options.id ||
|
||||
generateId("group_layer_") ||
|
||||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
@@ -4276,24 +4291,28 @@ export class RemoveChildLayerCommand extends Command {
|
||||
}
|
||||
// 恢复子图层到原位置
|
||||
this.parentLayer.children.splice(this.childIndex, 0, this.removedChild);
|
||||
optimizeCanvasRendering(this.canvas, async () => {
|
||||
this.originalObjects.forEach((obj) => {
|
||||
// 恢复对象到画布
|
||||
this.canvas.add(obj);
|
||||
// 恢复对象的图层信息
|
||||
obj.layerId = this.layerId;
|
||||
obj.layerName = this.removedChild.name;
|
||||
obj.setCoords(); // 更新坐标
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
optimizeCanvasRendering(this.canvas, async () => {
|
||||
this.originalObjects.forEach((obj) => {
|
||||
// 恢复对象到画布
|
||||
this.canvas.add(obj);
|
||||
// 恢复对象的图层信息
|
||||
obj.layerId = this.layerId;
|
||||
obj.layerName = this.removedChild.name;
|
||||
obj.setCoords(); // 更新坐标
|
||||
});
|
||||
|
||||
// 如果是原活动图层,恢复活动图层
|
||||
if (this.isActiveLayer) {
|
||||
this.activeLayerId.value = this.layerId;
|
||||
}
|
||||
// 如果是原活动图层,恢复活动图层
|
||||
if (this.isActiveLayer) {
|
||||
this.activeLayerId.value = this.layerId;
|
||||
}
|
||||
|
||||
// 重新渲染画布
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
// 重新渲染画布
|
||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
@@ -4434,3 +4453,90 @@ export class ChildLayerLockCommand extends Command {
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置图层混合模式
|
||||
*/
|
||||
export class SetLayerCompositeCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "设置图层混合模式",
|
||||
saveState: false,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.layerManager = options.layerManager;
|
||||
this.layerId = options.layerId;
|
||||
this.newValue = options.newValue;
|
||||
this.oldValue = options.oldValue;
|
||||
}
|
||||
|
||||
execute(isUndo = false) {
|
||||
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||
const { object } = findObjectByLayerId(this.canvas, this.layerId);
|
||||
if (!layer || !object) {
|
||||
console.error(`图层${this.layerId}不存在`);
|
||||
return false;
|
||||
}
|
||||
// console.log("==========", this.newValue, this.oldValue);
|
||||
const value = isUndo ? this.oldValue : this.newValue;
|
||||
layer.blendMode = value;
|
||||
object.set("globalCompositeOperation", value);
|
||||
this.canvas.renderAll();
|
||||
const event = isUndo ? "object:composite:undo" : "object:composite:execute";
|
||||
EventManager.emit(event, object);
|
||||
return true;
|
||||
}
|
||||
|
||||
undo() {
|
||||
return this.execute(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置颜色图层颜色
|
||||
*/
|
||||
export class SetColorLayerFillCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: "设置颜色图层颜色",
|
||||
saveState: false,
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layerManager = options.layerManager;
|
||||
this.object = options.object;
|
||||
this.layer = this.layerManager?.getLayerById(this.object.layerId);
|
||||
this.newFill = options.newFill;
|
||||
this.oldFill = JSON.parse(JSON.stringify(this.object.fill));
|
||||
this.layer.blendMode = "multiply";
|
||||
this.object.set("globalCompositeOperation", "multiply");
|
||||
this.object.set("originColor", options.originColor);
|
||||
}
|
||||
|
||||
async execute(isUndo = false) {
|
||||
if (!this.object) {
|
||||
console.error(`颜色图层不存在`);
|
||||
return false;
|
||||
}
|
||||
const isVisible = this.layer?.visible;
|
||||
if(!isVisible && this.layer) this.layer.visible = true;
|
||||
const gradient = new fabric.Gradient({
|
||||
type: "linear",
|
||||
gradientUnits: "percentage",
|
||||
...(isUndo ? this.oldFill : this.newFill),
|
||||
});
|
||||
this.object.setFill(gradient);
|
||||
this.canvas.renderAll();
|
||||
await this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
|
||||
this.object.id
|
||||
);
|
||||
if(!isVisible && this.layer) this.layer.visible = false;
|
||||
this.layerManager?.updateLayersObjectsInteractivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.execute(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
50
src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Command } from "./Command.js";
|
||||
|
||||
/**
|
||||
* 对象移动命令
|
||||
* 轻量级命令,只记录对象的移动属性变化(位置)
|
||||
*/
|
||||
export class ObjectMoveCommand extends Command {
|
||||
constructor(options) {
|
||||
super({
|
||||
name: options.name || "对象移动",
|
||||
description: options.description || "移动对象",
|
||||
saveState: false, // 自己管理状态,避免递归
|
||||
});
|
||||
|
||||
this.canvas = options.canvas;
|
||||
this.initPos = options.initPos;
|
||||
this.finalPos = options.finalPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
async execute() {
|
||||
this.setObjectsPos(this.finalPos);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 撤销命令
|
||||
* 应用初始状态
|
||||
*/
|
||||
async undo() {
|
||||
this.setObjectsPos(this.initPos);
|
||||
return true;
|
||||
}
|
||||
|
||||
async setObjectsPos(pos) {
|
||||
const objects = this.canvas.getObjects();
|
||||
const arr = typeof pos === "object" ? [pos] : pos;
|
||||
arr.forEach((item) => {
|
||||
const obj = objects.find((o) => o.id === item.id);
|
||||
if(obj) {
|
||||
obj.set({
|
||||
left: item.left,
|
||||
top: item.top,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
@@ -234,7 +234,7 @@ export class AddObjectToLayerCommand extends Command {
|
||||
parent.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||
this.fabricObject.clipPath = clippingMaskFabricObject;
|
||||
// 标记为脏对象
|
||||
|
||||
@@ -46,13 +46,13 @@ export class RasterizeLayerCommand extends Command {
|
||||
this.layerId
|
||||
);
|
||||
this.layer = layer;
|
||||
this.parentLayer = parent;
|
||||
// this.parentLayer = parent;
|
||||
|
||||
// 新增:如果有父图层,则栅格化父图层及其所有子图层
|
||||
if (this.parentLayer) {
|
||||
this.layer = this.parentLayer;
|
||||
this.layerId = this.parentLayer.id;
|
||||
}
|
||||
// // 新增:如果有父图层,则栅格化父图层及其所有子图层
|
||||
// if (this.parentLayer) {
|
||||
// this.layer = this.parentLayer;
|
||||
// this.layerId = this.parentLayer.id;
|
||||
// }
|
||||
|
||||
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
||||
|
||||
@@ -153,7 +153,7 @@ export class RasterizeLayerCommand extends Command {
|
||||
});
|
||||
|
||||
// 恢复原始图层结构
|
||||
this.layers.value = [...this.originalLayerStructure];
|
||||
this.layers.value = JSON.parse(this.originalLayerStructure);
|
||||
|
||||
// 恢复原活动图层
|
||||
this.activeLayerId.value = this.layerId;
|
||||
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
|
||||
*/
|
||||
_saveOriginalLayerStructure() {
|
||||
// 只保存相关的图层结构,而不是整个图层数组
|
||||
this.originalLayerStructure = JSON.parse(JSON.stringify(this.layers.value));
|
||||
this.originalLayerStructure = JSON.stringify(this.layers.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,12 +517,12 @@ export class ExportLayerToImageCommand extends Command {
|
||||
this.canvas
|
||||
);
|
||||
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
clippingMaskFabricObject.dirty = true;
|
||||
// clippingMaskFabricObject.dirty = true;
|
||||
clippingMaskFabricObject.setCoords();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { findObjectById } from "../utils/helper";
|
||||
import { findLayerRecursively } from "../utils/layerHelper";
|
||||
import { restoreFabricObject } from "../utils/objectHelper";
|
||||
import { Command } from "./Command";
|
||||
import EventManager from "../utils/event.js";
|
||||
|
||||
/**
|
||||
* 对象变换命令
|
||||
@@ -75,7 +76,7 @@ export class TransformCommand extends Command {
|
||||
|
||||
// 触发画布更新
|
||||
this.canvas.renderAll();
|
||||
|
||||
EventManager.emit("object:modified:execute", targetObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ export class TransformCommand extends Command {
|
||||
}, 300);
|
||||
// 触发画布更新
|
||||
this.canvas.renderAll();
|
||||
|
||||
EventManager.emit("object:modified:undo", targetObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ export class TransformCommand extends Command {
|
||||
);
|
||||
|
||||
if (clippingMaskFabricObject) {
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
@@ -493,7 +493,7 @@ export class CreateTextCommand extends Command {
|
||||
// 先在一级图层中查找
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
|
||||
if (layer.isPrintTrimsGroup) continue;
|
||||
if (layer.id === layerId) {
|
||||
return {
|
||||
layer: layer,
|
||||
|
||||
@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<!-- 图片列表面板 -->
|
||||
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
|
||||
<div class="crop-image-modal">
|
||||
<div class="modal-header">
|
||||
@@ -392,7 +391,7 @@
|
||||
<style scoped lang="less">
|
||||
/* 弹窗遮罩层 */
|
||||
.crop-image-overlay {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -420,8 +419,8 @@
|
||||
.crop-image-modal {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { ref, nextTick, computed, inject } from "vue";
|
||||
import { Checkbox } from "ant-design-vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { isGroupLayer } from "../../utils/layerHelper";
|
||||
import { isGroupLayer, SpecialLayerId } from "../../utils/layerHelper";
|
||||
import { fillToCssStyle, palletToFill, fillToPallet } from "../../utils/helper";
|
||||
import { SetColorLayerFillCommand } from "../../commands/LayerCommands";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const {t} = useI18n()
|
||||
// 设置组件名称,用于递归渲染
|
||||
@@ -183,6 +185,9 @@ function handleToggleVisibility() {
|
||||
}
|
||||
|
||||
function handleToggleLock() {
|
||||
// 禁用解锁的图层不能操作
|
||||
if (props.layer.isDisableUnlock) return;
|
||||
|
||||
if (props.isChild) {
|
||||
// 子图层需要传递父图层ID - 从父级组件获取
|
||||
const parentId = props.layer.parentId || findParentLayerId();
|
||||
@@ -348,6 +353,29 @@ function findParentLayerId() {
|
||||
console.warn("无法找到图层的父图层:", props.layer.id);
|
||||
return null;
|
||||
}
|
||||
const canvasManager = inject('canvasManager');
|
||||
const layerObject = computed(() => {
|
||||
const layer = props.layer;
|
||||
const id = layer.fabricObject?.id || layer.fabricObjects?.[0]?.id || layer.id;
|
||||
return canvasManager.getLayerObjectById(id);
|
||||
});
|
||||
const palletPanel = inject("palletPanel");
|
||||
const clickColor = () => {
|
||||
const fill = layerObject.value.fill;
|
||||
if (fill) {
|
||||
const obj = fillToPallet(fill);
|
||||
palletPanel(obj).then((res) => {
|
||||
const cmd = new SetColorLayerFillCommand({
|
||||
canvas: canvasManager.canvas,
|
||||
layerManager: layerManager,
|
||||
object: layerObject.value,
|
||||
newFill: palletToFill(res),
|
||||
originColor: res,
|
||||
});
|
||||
layerManager.commandManager.execute(cmd);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -377,8 +405,8 @@ function findParentLayerId() {
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="layer-drag-handle" :title="$t('拖拽排序')">
|
||||
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
||||
<div class="layer-drag-handle" :title="$t('拖拽排序')" v-if="!isHidenDragHandle">
|
||||
<SvgIcon :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
||||
</div>
|
||||
|
||||
<!-- 图层头部 -->
|
||||
@@ -417,9 +445,18 @@ function findParentLayerId() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 颜色图层按钮 -->
|
||||
<div
|
||||
class="layer-color-btn"
|
||||
v-if="layer.id === SpecialLayerId.COLOR"
|
||||
@click.stop="clickColor"
|
||||
:style="{
|
||||
background: fillToCssStyle(layerObject.fill),
|
||||
}"
|
||||
></div>
|
||||
|
||||
<!-- 图层操作按钮 -->
|
||||
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
|
||||
<div class="layer-actions" >
|
||||
<!-- 可见性切换 -->
|
||||
<div
|
||||
class="visibility-btn"
|
||||
@@ -434,7 +471,7 @@ function findParentLayerId() {
|
||||
<span
|
||||
v-if="layer.locked"
|
||||
class="status-icon locked"
|
||||
:class="{ disabled: layer.isBackground || layer.isFixed }"
|
||||
:class="{ disabled: layer.isBackground || layer.isFixed || layer.isDisableUnlock || layer.isFixedOther }"
|
||||
:title="$t('锁定')"
|
||||
@click.stop="handleToggleLock"
|
||||
>
|
||||
|
||||
@@ -287,7 +287,7 @@ const canDeleteComputed = computed(() => {
|
||||
:is-child="isChild"
|
||||
:is-active="layer.id === activeLayerId"
|
||||
:is-selected="isLayerSelected(layer.id)"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:is-multi-select-mode="isMultiSelectMode && !(layer.isPrintTrims || layer.isPrintTrimsGroup)"
|
||||
:is-editing="editingLayerId === layer.id"
|
||||
:editing-name="editingLayerName"
|
||||
:can-delete="
|
||||
@@ -296,7 +296,7 @@ const canDeleteComputed = computed(() => {
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
@click="(...args) => forwardEvent('layer-click', ...args)"
|
||||
@double-click="(...args) => forwardEvent('layer-double-click', ...args)"
|
||||
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
|
||||
@context-menu="(...args) => !(layer.isPrintTrims || layer.isPrintTrimsGroup) && forwardEvent('context-menu', ...args)"
|
||||
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
|
||||
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
|
||||
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
|
||||
@@ -385,17 +385,10 @@ const canDeleteComputed = computed(() => {
|
||||
<style scoped lang="less">
|
||||
// 从父组件的样式文件中继承相关样式
|
||||
.layers-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.sortable-layers {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
// .layer-group {
|
||||
// // margin-bottom: 1px;
|
||||
// }
|
||||
|
||||
.child-layers {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
|
||||
@@ -81,14 +81,14 @@ const fillColorRef = ref(null);
|
||||
// 计算属性:可排序的根级图层(排除背景层和固定层)
|
||||
const sortableRootLayers = computed(() => {
|
||||
if (!layers) return [];
|
||||
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
|
||||
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground && !layer.isFixedOther);
|
||||
});
|
||||
|
||||
// 计算属性:不可排序的固定图层(背景层和固定层)
|
||||
const fixedLayers = computed(() => {
|
||||
if (!layers) return [];
|
||||
return layers.value.filter((layer) => {
|
||||
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
|
||||
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground || layer.isFixedOther);
|
||||
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
|
||||
});
|
||||
});
|
||||
@@ -576,7 +576,7 @@ function handleLayerClick(layer, event) {
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
|
||||
toggleLayerSelection(layer, event);
|
||||
} else {
|
||||
lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
||||
if(!layer.isPrintTrimsGroup) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
||||
// 普通点击:进入单选模式
|
||||
// selectedLayerIds.value = [layer.id];
|
||||
// isMultiSelectMode.value = false;
|
||||
@@ -596,7 +596,7 @@ function handleLayerClick(layer, event) {
|
||||
layerManager?.updateLayersObjectsInteractivity();
|
||||
}
|
||||
}
|
||||
lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
||||
if(!layer.isPrintTrimsGroup) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,7 +999,7 @@ function buildChildLayerContextMenuItems(childLayer) {
|
||||
{
|
||||
label: childLayer.locked ? "解锁图层" : "锁定图层",
|
||||
icon: childLayer.locked ? "CUnLock" : "CLock",
|
||||
disabled: childLayer.isBackground || childLayer.isFixed,
|
||||
disabled: childLayer.isBackground || childLayer.isFixed || childLayer.isDisableUnlock,
|
||||
action: () => toggleChildLayerLock(childLayer.id),
|
||||
},
|
||||
// 显示/隐藏
|
||||
@@ -1240,6 +1240,12 @@ async function handleCrossLevelMove(moveData) {
|
||||
}
|
||||
|
||||
try {
|
||||
const layer = findLayerRecursively(layers.value, layerId).layer;
|
||||
const toLayer = findLayerRecursively(layers.value, toParentId).layer;
|
||||
if(layer?.isPrintTrims || layer?.isPrintTrimsGroup || toLayer?.isPrintTrims || toLayer?.isPrintTrimsGroup) {
|
||||
console.warn("当前图层不可移动到外部");
|
||||
return;
|
||||
}
|
||||
// 如果有命令管理器,使用命令模式
|
||||
if (commandManager) {
|
||||
console.log("📝 使用命令模式执行跨层级移动");
|
||||
@@ -1593,47 +1599,48 @@ async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex
|
||||
<small>{{ $t('Canvas.Hint') }}</small>
|
||||
</div>
|
||||
|
||||
<div class="layers-list-container">
|
||||
<!-- 图层列表组件 -->
|
||||
<LayersList
|
||||
:layers="layers"
|
||||
:active-layer-id="activeLayerId"
|
||||
:sortable-root-layers="sortableRootLayers"
|
||||
:fixed-layers="fixedLayers"
|
||||
:selected-layer-ids="selectedLayerIds"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:editing-layer-id="editingLayerId"
|
||||
:editing-layer-name="editingLayerName"
|
||||
:thumbnail-manager="thumbnailManager"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
:isChild="false"
|
||||
group-name="layers-root"
|
||||
@layer-click="handleLayerClick"
|
||||
@layer-double-click="handleLayerDoubleClick"
|
||||
@context-menu="showContextMenu"
|
||||
@checkbox-change="handleCheckboxClick"
|
||||
@toggle-visibility="toggleLayerVisibility"
|
||||
@toggle-lock="toggleSelectedLayersLockByLayer"
|
||||
@delete="removeLayer"
|
||||
@edit-confirm="confirmEdit"
|
||||
@edit-cancel="cancelEdit"
|
||||
@edit-keydown="handleEditKeydown"
|
||||
@touch-start="handleTouchStart"
|
||||
@touch-move="handleTouchMove"
|
||||
@touch-end="handleTouchEnd"
|
||||
@update:editing-name="editingLayerName = $event"
|
||||
@root-layers-sort="handleRootLayersSort"
|
||||
@child-layers-sort="handleChildLayersSort"
|
||||
@cross-level-move="handleCrossLevelMove"
|
||||
@select-child-layer="selectChildLayer"
|
||||
@start-child-layer-edit="startChildLayerEdit"
|
||||
@child-context-menu="showChildLayerContextMenu"
|
||||
@toggle-group-expanded="toggleGroupExpanded"
|
||||
@toggle-child-visibility="toggleChildLayerVisibility"
|
||||
@toggle-child-lock="toggleChildLayerLock"
|
||||
@delete-child="deleteChildLayer"
|
||||
@rename-child="renameChildLayer"
|
||||
/>
|
||||
|
||||
<LayersList
|
||||
:layers="layers"
|
||||
:active-layer-id="activeLayerId"
|
||||
:sortable-root-layers="sortableRootLayers"
|
||||
:fixed-layers="fixedLayers"
|
||||
:selected-layer-ids="selectedLayerIds"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:editing-layer-id="editingLayerId"
|
||||
:editing-layer-name="editingLayerName"
|
||||
:thumbnail-manager="thumbnailManager"
|
||||
:expanded-group-ids="expandedGroupIds"
|
||||
:isChild="false"
|
||||
group-name="layers-root"
|
||||
@layer-click="handleLayerClick"
|
||||
@layer-double-click="handleLayerDoubleClick"
|
||||
@context-menu="showContextMenu"
|
||||
@checkbox-change="handleCheckboxClick"
|
||||
@toggle-visibility="toggleLayerVisibility"
|
||||
@toggle-lock="toggleSelectedLayersLockByLayer"
|
||||
@delete="removeLayer"
|
||||
@edit-confirm="confirmEdit"
|
||||
@edit-cancel="cancelEdit"
|
||||
@edit-keydown="handleEditKeydown"
|
||||
@touch-start="handleTouchStart"
|
||||
@touch-move="handleTouchMove"
|
||||
@touch-end="handleTouchEnd"
|
||||
@update:editing-name="editingLayerName = $event"
|
||||
@root-layers-sort="handleRootLayersSort"
|
||||
@child-layers-sort="handleChildLayersSort"
|
||||
@cross-level-move="handleCrossLevelMove"
|
||||
@select-child-layer="selectChildLayer"
|
||||
@start-child-layer-edit="startChildLayerEdit"
|
||||
@child-context-menu="showChildLayerContextMenu"
|
||||
@toggle-group-expanded="toggleGroupExpanded"
|
||||
@toggle-child-visibility="toggleChildLayerVisibility"
|
||||
@toggle-child-lock="toggleChildLayerLock"
|
||||
@delete-child="deleteChildLayer"
|
||||
@rename-child="renameChildLayer"
|
||||
/>
|
||||
</div>
|
||||
<!-- 固定层(背景层和固定层) -->
|
||||
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
||||
<!-- 遍历固定层 -->
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
z-index: 6;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
// max-height: 70vh;
|
||||
overflow: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -161,12 +161,12 @@
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.layers-list-container{
|
||||
overflow-y: auto;
|
||||
}
|
||||
// 图层列表
|
||||
.layers-list {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// 图层项样式
|
||||
@@ -340,6 +340,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.layer-color-btn{
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
// 图层操作
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -384,7 +384,7 @@ async function prepareForLiquify(targetObj) {
|
||||
}
|
||||
updateAllParams();
|
||||
|
||||
console.log("液化环境准备完成");
|
||||
console.log("液化环境准备完成",compositeCommand);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("准备液化环境失败:", error);
|
||||
@@ -1614,6 +1614,7 @@ function close() {
|
||||
*/
|
||||
function startPressTimer() {
|
||||
if (pressTimer.value) return;
|
||||
if (currentMode.value === compositeCommand.value.liquifyManager.enhancedManager.modes.PUSH) return;
|
||||
|
||||
pressTimer.value = setInterval(() => {
|
||||
// 计算按压持续时间
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<!-- 颜色选择器模板 -->
|
||||
<div v-show="showPanel" class="pallet-overlay" @click.self="close">
|
||||
<div class="pallet-modal">
|
||||
<!-- <div class="modal-header">
|
||||
<h3></h3>
|
||||
<button class="close-btn" @click="close">×</button>
|
||||
</div> -->
|
||||
|
||||
<div class="modal-content">
|
||||
<pallet
|
||||
v-if="showPanel"
|
||||
:selectColor="selectColor"
|
||||
@selectUplpadColor="selectUplpadColor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="image-count" @click="close">
|
||||
{{ $t("Canvas.close") }}
|
||||
</div>
|
||||
<div class="image-submit gallery_btn" @click="confirm">
|
||||
{{ $t("Canvas.confirm") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
defineProps,
|
||||
onDeactivated,
|
||||
reactive,
|
||||
onMounted,
|
||||
defineExpose,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import pallet from "./pallet.vue";
|
||||
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Props
|
||||
const props = defineProps([]);
|
||||
const { t } = useI18n();
|
||||
var resolveFn: (value: any) => void;
|
||||
const showPanel = ref(false);
|
||||
const open = (obj = {}) => {
|
||||
selectColor.value = JSON.parse(JSON.stringify(obj));
|
||||
showPanel.value = true;
|
||||
return new Promise((resolve) => (resolveFn = resolve));
|
||||
};
|
||||
const close = () => {
|
||||
showPanel.value = false;
|
||||
};
|
||||
//提交选中的T图片
|
||||
const confirm = () => {
|
||||
close();
|
||||
resolveFn && resolveFn(JSON.parse(JSON.stringify(selectColor.value)));
|
||||
};
|
||||
const selectColor = ref({});
|
||||
const selectUplpadColor = (item: any) => {
|
||||
selectColor.value = JSON.parse(JSON.stringify(item));
|
||||
};
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 弹窗遮罩层 */
|
||||
.pallet-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1001;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗主体 */
|
||||
.pallet-modal {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
max-width: 90%;
|
||||
max-height: 95%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
animation: modalSlideUp 0.3s ease;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@keyframes modalSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗头部 */
|
||||
.modal-header {
|
||||
padding: 16px 20px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: #333;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗内容 */
|
||||
.modal-content {
|
||||
width: 35rem;
|
||||
// max-width: 240px;
|
||||
margin: 10px 20px;
|
||||
background-color: #fff;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 弹窗底部 */
|
||||
.modal-footer {
|
||||
padding: 10px 20px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
> .image-submit {
|
||||
font-size: 1.2rem;
|
||||
line-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,666 @@
|
||||
<template>
|
||||
<div class="pallet" ref="palletRef">
|
||||
<div class="palletColo" @click="openPallet">
|
||||
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
||||
{{ selectColor.hex }}
|
||||
</div>
|
||||
<div v-show="selectColor.gradient" class="palletBackColor" :style="{'background-image':`linear-gradient(${selectColor.gradient?.angle}deg,${setGradient(selectColor.gradient)})`}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="palletBox">
|
||||
<div class="color_setting_block" @click.stop>
|
||||
<Chrome class="chrome_color" v-model="color_"></Chrome>
|
||||
<div class="color_setting_operateSingle">
|
||||
<div class="color_setting_btn" :class="{active:!color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Single') }}</div>
|
||||
<a-switch :checked="color?.gradient?.gradientShow" @click="setOperate"/>
|
||||
<div class="color_setting_btn" :class="{active:color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Gradual') }}</div>
|
||||
</div>
|
||||
<div class="color_setting_operate" v-if="color?.gradient?.gradientShow">
|
||||
<div class="color_setting_operate_item color_setting_operate_control">
|
||||
<div class="operate_item_box">
|
||||
<div>{{ $t('ColorboardUpload.Alignment') }}</div>
|
||||
</div>
|
||||
<div class="operate_item_box operate_item_angle">
|
||||
<div class="operate_item_angle_box" @mousedown="mousedownGradientAngle(getMousePosition($event,false))" @touchstart="mousedownGradientAngle(getMousePosition($event,true))">
|
||||
<div :style="{'transform':`rotate(${color.gradient.angle}deg)`}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operate_item_box operate_item_delete">
|
||||
<i class="fi fi-rr-trash" @click="deleteGradientItem"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="color_setting_operate_item color_setting_operate_input">
|
||||
<div class="color_setting_operate_bg" @click="addGradient($event)" :style="{'background-image':color?.gradient?`linear-gradient(90deg,${setGradient(color.gradient)})`:'none'}">
|
||||
</div>
|
||||
<div v-for="item,index in color.gradient.gradientList" :key="item" class="color_setting_operate_btn" :class="{'active':index == color.gradient.selectIndex}" :style="{'left':item.left,'background-color':`rgba(${item.rgba.r},${item.rgba.g},${item.rgba.b},${item.rgba.a})`}" @mousedown="mousedownGradient(getMousePosition($event,false),item,index,)" @touchstart="mousedownGradient(getMousePosition($event,true),item,index,)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent,computed,ref,watch,nextTick,onMounted,onUnmounted,toRefs, reactive} from 'vue'
|
||||
import { useStore } from "vuex";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { message,Upload} from 'ant-design-vue';
|
||||
import { Sketch, Chrome} from '@ans1998/vue3-color'
|
||||
import { getMousePosition } from "@/tool/mdEvent";
|
||||
import { rgbaToHex } from "@/tool/util"
|
||||
import { color } from 'echarts/core';
|
||||
export default defineComponent({
|
||||
components:{
|
||||
Chrome,
|
||||
},
|
||||
props:{
|
||||
selectColor:{
|
||||
type:Object,
|
||||
default:()=>{}
|
||||
},
|
||||
},
|
||||
emits:['selectUplpadColor'],
|
||||
setup(props,{emit}) {
|
||||
const {t} = useI18n()
|
||||
const store = useStore();
|
||||
const palletData = reactive({
|
||||
palletShow: true,
|
||||
palletList:[],
|
||||
color_:{} as any,
|
||||
color:{} as any,
|
||||
updataSelectColorTime:null as any,
|
||||
gradient:{
|
||||
gradientList:[
|
||||
{
|
||||
rgba:{
|
||||
r:117,
|
||||
g:119,
|
||||
b:255,
|
||||
a:1,
|
||||
},
|
||||
left:'0%'
|
||||
},{
|
||||
rgba:{
|
||||
r:0,
|
||||
g:222,
|
||||
b:152,
|
||||
a:1,
|
||||
},
|
||||
left:'100%'
|
||||
},
|
||||
],
|
||||
angle:45,
|
||||
selectIndex:-1,
|
||||
gradientShow:false,
|
||||
},
|
||||
setGradient:computed(()=>{
|
||||
return (gradient:any)=>{
|
||||
let gradientStr = ''
|
||||
if(!gradient?.gradientList)return
|
||||
gradient.gradientList.sort((a:any, b:any) => {
|
||||
let aArr = a.left.split('%')[0]
|
||||
let bArr = b.left.split('%')[0]
|
||||
return aArr - bArr;
|
||||
});
|
||||
gradient.gradientList.forEach((item:any,index:any)=>{
|
||||
let str = ','
|
||||
if(gradient.gradientList.length == index+1)str = ''
|
||||
let rgba = item.rgba?item.rgba:{r:255,g:255,b:255}
|
||||
gradientStr += `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a}) ${item.left}${str}`
|
||||
|
||||
})
|
||||
return `${gradientStr}`
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
const getpalletListDom = reactive({
|
||||
})
|
||||
const palletRef = ref(null)
|
||||
watch(()=>palletData.color_,(newVal:any)=>{
|
||||
if(!newVal?.rgba?.r)return
|
||||
if(palletData.color?.gradient?.gradientShow){
|
||||
palletData.color.gradient.gradientList[palletData.color.gradient.selectIndex].rgba = {
|
||||
r:newVal.rgba.r,
|
||||
g:newVal.rgba.g,
|
||||
b:newVal.rgba.b,
|
||||
a:newVal.rgba.a,
|
||||
}
|
||||
}else{
|
||||
palletData.color = newVal
|
||||
}
|
||||
})
|
||||
watch(()=>palletData.color,(newVal:any)=>{
|
||||
if(JSON.stringify(props.selectColor) != JSON.stringify(newVal)){
|
||||
newVal.name = ''
|
||||
newVal.tcx = ''
|
||||
let rgba = [newVal.rgba.r,newVal.rgba.g,newVal.rgba.b]
|
||||
let hex = rgbaToHex(rgba)
|
||||
newVal.hex = hex
|
||||
emit('selectUplpadColor',newVal)
|
||||
}
|
||||
},{deep: true })
|
||||
const setOperate = ()=>{
|
||||
if(!palletData.color.rgba)return message.info(t('DesignDetailAlter.jsContent7'))
|
||||
palletData.color.rgba = palletData.color?.rgba?.r?palletData.color.rgba:{r:0,g:0,b:0,a:1}
|
||||
palletData.gradient.selectIndex = 0
|
||||
palletData.gradient.gradientShow = true
|
||||
if(!palletData.color.gradient){
|
||||
if(palletData.color.rgba.r){
|
||||
palletData.gradient.gradientList[palletData.gradient.selectIndex].rgba = {
|
||||
r:palletData.color.rgba.r,
|
||||
g:palletData.color.rgba.g,
|
||||
b:palletData.color.rgba.b,
|
||||
a:1,
|
||||
}
|
||||
}
|
||||
palletData.color.gradient = JSON.parse(JSON.stringify(palletData.gradient))
|
||||
}else{
|
||||
palletData.color.rgba = palletData.color.gradient.gradientList[0].rgba
|
||||
palletData.color.gradient = null
|
||||
}
|
||||
}
|
||||
const deleteGradientItem = ()=>{
|
||||
if(palletData.color.gradient.gradientList.length <= 2)return
|
||||
palletData.color.gradient.gradientList.splice(palletData.color.gradient.selectIndex,1)
|
||||
}
|
||||
const addGradient = (event:any)=>{
|
||||
let gradientWidth = event.target.clientWidth
|
||||
let left:any = event.offsetX/gradientWidth
|
||||
palletData.color.gradient.gradientList.push({
|
||||
rgba:palletData.color_.rgba,
|
||||
left:left.toFixed(2)*100+'%'
|
||||
})
|
||||
}
|
||||
const mousedownGradientAngle = (event:any)=>{
|
||||
// isMoible() true为移动端
|
||||
let domPosition = event.target.getBoundingClientRect()
|
||||
let position = {
|
||||
x:domPosition.x+domPosition.width/2,
|
||||
y:domPosition.y+domPosition.height/2,
|
||||
}
|
||||
let angle
|
||||
let mousedown = function(event:any){
|
||||
let e = getMousePosition(event,false)
|
||||
mouseDownOperation(e)
|
||||
}
|
||||
|
||||
let touchstart = function(event:any){
|
||||
let e = getMousePosition(event,true)
|
||||
mouseDownOperation(e)
|
||||
}
|
||||
let mouseDownOperation = (e:any)=>{
|
||||
let X = position.x
|
||||
let Y = position.y
|
||||
let x = (e.clientX) - X
|
||||
let y = Y -( e.clientY)
|
||||
angle = Math.atan2(x,y)*(180 / Math.PI)
|
||||
// this.colorList[this.selectIndex].gradient = JSON.parse(JSON.stringify(this.gradient))
|
||||
palletData.color.gradient.angle = angle
|
||||
|
||||
}
|
||||
let mouseupGradientAngle = ()=>{
|
||||
window.removeEventListener('touchmove',touchstart)
|
||||
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||
|
||||
window.removeEventListener('mousemove',mousedown)
|
||||
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||
}
|
||||
window.addEventListener('touchmove',touchstart)
|
||||
window.addEventListener('touchend',mouseupGradientAngle)
|
||||
|
||||
window.addEventListener('mousemove',mousedown)
|
||||
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||
}
|
||||
|
||||
const mousedownGradient = (event:any,item:any,index:number)=>{
|
||||
palletData.color.gradient.selectIndex = index
|
||||
|
||||
|
||||
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
||||
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
|
||||
let position = {
|
||||
x:event.clientX,
|
||||
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
||||
}
|
||||
let mousedown = function(event:any){
|
||||
let e = getMousePosition(event,false)
|
||||
mousedownGradient(e)
|
||||
}
|
||||
|
||||
let touchstart = function(event:any){
|
||||
let e = getMousePosition(event,true)
|
||||
mousedownGradient(e)
|
||||
}
|
||||
let mousedownGradient = (e:any)=>{
|
||||
let left = ((e.clientX) - position.x)/gradientWidth*100+Number(position.left)
|
||||
left = (left<0?0:left>100?100:left)
|
||||
item.left = left+'%'
|
||||
}
|
||||
|
||||
let mouseupGradientAngle = ()=>{
|
||||
window.removeEventListener('touchmove',touchstart)
|
||||
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||
window.removeEventListener('mousemove',mousedown)
|
||||
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||
}
|
||||
window.addEventListener('touchmove',touchstart)
|
||||
window.addEventListener('touchend',mouseupGradientAngle)
|
||||
|
||||
window.addEventListener('mousemove',mousedown)
|
||||
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||
|
||||
|
||||
}
|
||||
const selectImgItem = ()=>{
|
||||
|
||||
}
|
||||
const openPallet = ()=>{
|
||||
if(palletData.palletShow && props.selectColor?.rgba?.r){
|
||||
if(props.selectColor.gradient){
|
||||
palletData.color_.rgba = props.selectColor.gradient.gradientList[0].rgba
|
||||
}else{
|
||||
palletData.color_ = JSON.parse(JSON.stringify(props.selectColor))
|
||||
palletData.gradient.gradientShow = false
|
||||
}
|
||||
|
||||
palletData.color = JSON.parse(JSON.stringify(props.selectColor))
|
||||
}else{
|
||||
}
|
||||
}
|
||||
// 点击外部区域关闭颜色选择器
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
|
||||
const palletColo = palletRef.value.querySelector('.palletColo');
|
||||
|
||||
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
||||
if (palletData.palletShow && colorSettingBlock &&
|
||||
!colorSettingBlock.contains(target) &&
|
||||
!palletColo?.contains(target)) {
|
||||
palletData.palletShow = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
// 添加点击外部区域监听器
|
||||
// document.addEventListener('click', handleClickOutside);
|
||||
|
||||
nextTick().then(()=>{
|
||||
const backIcon = document.createElement('div');
|
||||
backIcon.classList.add('vc-sketch-color-wrap')
|
||||
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
||||
dropperDom.appendChild(backIcon);
|
||||
backIcon.addEventListener('click',async ()=>{
|
||||
try {
|
||||
const dropper = new EyeDropper();
|
||||
const result = await dropper.open();
|
||||
let hex = result.sRGBHex.replace("#", "");
|
||||
// 将十六进制颜色码拆分成红、绿、蓝三个部分
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
palletData.color = {rgba:{r:r,g:g,b:b,a:1},hex:result.sRGBHex}
|
||||
// return `rgb(${r}, ${g}, ${b})`;
|
||||
// box.style.backgroundColor = label.textContent = result.sRGBHex;
|
||||
} catch (e) {
|
||||
message.info(t('DesignDetailAlter.jsContent1'))
|
||||
}
|
||||
})
|
||||
openPallet();
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
// 清理事件监听器
|
||||
// document.removeEventListener('click', handleClickOutside);
|
||||
})
|
||||
return{
|
||||
...toRefs(palletData),
|
||||
...toRefs(getpalletListDom),
|
||||
palletRef,
|
||||
openPallet,
|
||||
selectImgItem,
|
||||
setOperate,
|
||||
deleteGradientItem,
|
||||
addGradient,
|
||||
mousedownGradientAngle,
|
||||
mousedownGradient,
|
||||
getMousePosition,
|
||||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.pallet{
|
||||
// position: absolute;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
> .palletColo{
|
||||
width: 100%;
|
||||
height: 7rem;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid #000;
|
||||
padding: .5rem .6rem;
|
||||
cursor: pointer;
|
||||
> .palletBackColor{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
> .palletBox{
|
||||
margin-top: 1.5rem;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
> .color_setting_block{
|
||||
margin: auto;
|
||||
background: linear-gradient(70deg, #eee4f3, #f3f4e6);
|
||||
width: 100%;
|
||||
// border-radius: calc(1rem*1.2);
|
||||
overflow: hidden;
|
||||
box-shadow: 2px 2px 8px rgba(0,0,0,.3);
|
||||
.vc-chrome{
|
||||
background: rgba(0,0,0,0);
|
||||
box-shadow:none;
|
||||
}
|
||||
:deep(.chrome_color){
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.vc-chrome-saturation-wrap{
|
||||
width: 30rem;
|
||||
height: 30rem;
|
||||
margin: 2rem auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.vc-saturation-pointer{
|
||||
pointer-events: none;
|
||||
}
|
||||
.vc-chrome-body{
|
||||
padding: 0;
|
||||
width: 90%;
|
||||
margin: 2 auto;
|
||||
margin: 0 auto;
|
||||
background: rgba(0,0,0,0);
|
||||
margin-bottom: 3rem;
|
||||
// display: none;
|
||||
.vc-chrome-fields-wrap{
|
||||
margin-top: 5%;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
.vc-chrome-toggle-btn{
|
||||
width: calc(3.2rem*1.2);
|
||||
.vc-chrome-toggle-icon{
|
||||
height: auto;
|
||||
margin-right: calc(-0.4rem*1.2);
|
||||
margin-top: calc(0rem*1.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
svg{
|
||||
width: calc(2.4rem*1.2) !important;
|
||||
height: calc(2.4rem*1.2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.vc-chrome-fields{
|
||||
.vc-chrome-field{
|
||||
padding-left: calc(.6rem*1.2);
|
||||
}
|
||||
.vc-input__label{
|
||||
font-size: calc(1.6rem*1.2);
|
||||
}
|
||||
.vc-input__input{
|
||||
font-size: 2rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
.ant-upload-list{
|
||||
|
||||
}
|
||||
.vc-sketch-color-wrap{
|
||||
background-image: url(@/assets/images/homePage/dropper.png);
|
||||
background-size: 3rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
padding: calc(.7rem*1.2);
|
||||
border: 1px solid;
|
||||
position: absolute;
|
||||
bottom: -2rem;
|
||||
right: 0rem;
|
||||
border-radius: calc(.5rem*1.2);
|
||||
|
||||
}
|
||||
.vc-chrome-fields{
|
||||
.vc-input__label{
|
||||
margin-top: calc(1rem*1.2);
|
||||
}
|
||||
}
|
||||
.vc-chrome-fields:nth-child(2){
|
||||
>:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.vc-chrome-fields:nth-child(3){
|
||||
>:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.vc-chrome-controls{
|
||||
align-items: center;
|
||||
flex-direction: row-reverse;
|
||||
.vc-chrome-color-wrap{
|
||||
// width: 3.6rem*1.2);
|
||||
margin-left: calc(2rem*1.2);
|
||||
width: auto;
|
||||
.vc-chrome-active-color{
|
||||
border-radius: 50%;
|
||||
}
|
||||
.vc-chrome-active-color,.vc-checkerboard{
|
||||
width: calc(3rem*1.2);
|
||||
height: calc(3rem*1.2);
|
||||
}
|
||||
}
|
||||
.vc-chrome-hue-wrap,.vc-chrome-alpha-wrap{
|
||||
.vc-hue{
|
||||
border-radius: 15px;
|
||||
}
|
||||
.vc-alpha{
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
height: 2rem;
|
||||
margin: 0;
|
||||
.vc-hue-pointer{
|
||||
transform: translateX(-1.25rem);
|
||||
}
|
||||
.vc-hue-picker{
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
transform: translate(0px,-3px);
|
||||
}
|
||||
}
|
||||
.vc-chrome-alpha-wrap{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.vc-chrome-saturation-wrap .vc-saturation-circle{
|
||||
width: calc(1rem*1.2);
|
||||
height: calc(1rem*1.2);
|
||||
}
|
||||
}
|
||||
.color_block{
|
||||
// margin-top: calc(1rem;
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// font-size: calc(1.6rem;
|
||||
width: 100%;
|
||||
padding: 0 5%;
|
||||
padding-bottom: 5%;
|
||||
margin: calc(0.5rem*1.2) auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.color_right{
|
||||
width: 13rem;
|
||||
font-size: calc(1.2rem*1.2);
|
||||
color: #666666;
|
||||
.color_rgb_block{
|
||||
display: flex;
|
||||
.rgb_item{
|
||||
margin-left: calc(.2rem*1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
.color_left{
|
||||
cursor: pointer;
|
||||
color: rgb(153, 153, 153);
|
||||
}
|
||||
.color_right,.color_left{
|
||||
>div{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.color_HEX_block,.color_rgb_block{
|
||||
padding: .25rem .6rem;
|
||||
box-shadow: inset 0 0 0 1px #ccc;
|
||||
border-radius: .5rem;
|
||||
justify-content: space-around;
|
||||
text-transform:uppercase;
|
||||
.color_block_bg{
|
||||
width: 1.8rem;
|
||||
height: 2.5rem;
|
||||
// margin-right: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.color_block_bg{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.color_setting_operateSingle{
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.color_setting_btn{
|
||||
margin: 0 1rem;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
&.active{
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color_setting_operate{
|
||||
*{
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
}
|
||||
.color_setting_operate_item{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
.operate_item_box{
|
||||
}
|
||||
}
|
||||
.color_setting_operate_control{
|
||||
.operate_item_delete,.operate_item_angle{
|
||||
cursor: pointer;
|
||||
}
|
||||
.operate_item_delete{
|
||||
i{
|
||||
display: flex;
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
.operate_item_angle{
|
||||
.operate_item_angle_box{
|
||||
border-radius: 50%;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border: solid 2px #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
>div{
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
position: relative;
|
||||
pointer-events:none;
|
||||
}
|
||||
>div::before{
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0.2rem;
|
||||
left: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.color_setting_operate_input{
|
||||
width: 80%;
|
||||
// padding: 0 10%;
|
||||
margin: 1.2rem 10%;
|
||||
border-radius: 10%;
|
||||
position: relative;
|
||||
height: 2.5rem;
|
||||
.color_setting_operate_bg{
|
||||
border-radius: .5rem;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
.color_setting_operate_btn{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
left: 0;
|
||||
width: 1rem;
|
||||
height: 110%;
|
||||
border: .2rem solid;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
box-sizing: content-box;
|
||||
z-index: 2;
|
||||
&.active{
|
||||
border: .3rem solid;
|
||||
}
|
||||
}
|
||||
.color_setting_operate_btn:hover{
|
||||
border: .3rem solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="part-selector-toolbar"
|
||||
v-if="visible"
|
||||
:class="{ active: !closePanel }"
|
||||
>
|
||||
<div class="btn" @click="setClosePanel">
|
||||
<i class="fi fi-br-angle-left"></i>
|
||||
</div>
|
||||
<!-- 顶部选区类型工具栏 -->
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-header">
|
||||
<div class="header-title">
|
||||
{{ t("Canvas.GarmentPartSelector") }}
|
||||
</div>
|
||||
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
||||
<div class="tip">
|
||||
<div>
|
||||
<img
|
||||
src="/src/assets/images/canvas/shubiao-l.png"
|
||||
/>
|
||||
<span>Left Click: Add</span>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/src/assets/images/canvas/shubiao-r.png"
|
||||
/>
|
||||
<span>Right Click: Remove</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-types">
|
||||
<div
|
||||
v-for="item in toolList"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: toolType === item.type },
|
||||
]"
|
||||
@click="setPartType(item.type)"
|
||||
>
|
||||
<svg-icon :name="item.icon" :size="item.size" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<!-- 底部选区操作工具栏 -->
|
||||
<div class="tool-actions">
|
||||
<div class="action-btn" @click="onCreate">
|
||||
<svg-icon name="CPaste" size="16" />
|
||||
<span class="btn-text">{{
|
||||
$t("Canvas.creation")
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="onCopyCreate">
|
||||
<svg-icon name="CCut" size="26" />
|
||||
<span class="btn-text">{{
|
||||
$t("Canvas.CreateAndCopy")
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
CreateSelectionCommand,
|
||||
InvertSelectionCommand,
|
||||
FeatherSelectionCommand,
|
||||
FillSelectionCommand,
|
||||
} from "../commands/SelectionCommands";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import {
|
||||
LassoCutoutCommand,
|
||||
ClearSelectionCommand,
|
||||
// CutSelectionToNewLayerCommand,
|
||||
} from "../commands/LassoCutoutCommand";
|
||||
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { ClearSelectionContentCommand } from "../commands/ClearSelectionContentCommand";
|
||||
import { CutSelectionToNewLayerCommand } from "../commands/CutSelectionToNewLayerCommand";
|
||||
|
||||
const props = defineProps({
|
||||
canvas: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commandManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
partManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
partManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layerManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
toolManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
const toolType = ref(OperationType.PART);
|
||||
//打开隐藏操作面板
|
||||
const closePanel = ref(false);
|
||||
const setClosePanel = () => {
|
||||
closePanel.value = !closePanel.value;
|
||||
};
|
||||
|
||||
const toolList = [
|
||||
{
|
||||
type: OperationType.PART,
|
||||
label: "Point Selection",
|
||||
icon: "CPoint",
|
||||
size: "20",
|
||||
},
|
||||
{
|
||||
type: OperationType.PART_RECTANGLE,
|
||||
label: "Marquee Selection",
|
||||
icon: "CMarquee",
|
||||
size: "20",
|
||||
},
|
||||
{
|
||||
type: OperationType.PART_BRUSH,
|
||||
label: "Brush Selection",
|
||||
icon: "CBrush2",
|
||||
size: "16",
|
||||
},
|
||||
{
|
||||
type: OperationType.PART_ERASER,
|
||||
label: "Erase",
|
||||
icon: "CEraser2",
|
||||
size: "22",
|
||||
},
|
||||
];
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {});
|
||||
|
||||
// 监听 activeTool 变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
// 当工具为LASSO或AREA类型时显示选区面板
|
||||
const selectionTools = [
|
||||
OperationType.PART,
|
||||
OperationType.PART_RECTANGLE,
|
||||
OperationType.PART_BRUSH,
|
||||
OperationType.PART_ERASER,
|
||||
];
|
||||
|
||||
if (selectionTools.includes(newTool)) {
|
||||
show();
|
||||
// 根据工具类型设置选区类型
|
||||
toolType.value = newTool;
|
||||
|
||||
// 更新选区管理器的选区类型
|
||||
// if (props.partManager) {
|
||||
// props.partManager.setPartType(toolType.value);
|
||||
// props.partManager.setupPartEvents();
|
||||
// }
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
visible.value = true;
|
||||
closePanel.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区类型
|
||||
*/
|
||||
function setPartType(type) {
|
||||
toolType.value = type;
|
||||
|
||||
// 通过 ToolManager 切换工具,这会自动通知 partManager
|
||||
if (props.toolManager) {
|
||||
props.toolManager.setToolWithCommand(type);
|
||||
}
|
||||
|
||||
// // 备用方案:如果没有 toolManager,直接更新 partManager
|
||||
// else if (props.partManager) {
|
||||
// props.partManager.setPartType(type);
|
||||
// props.partManager.setupPartEvents();
|
||||
// }
|
||||
}
|
||||
|
||||
// 创建
|
||||
function onCreate() {
|
||||
|
||||
}
|
||||
// 复制并创建
|
||||
function onCopyCreate() {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.part-selector-toolbar {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
max-width: min(90vw, 700px);
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
color: #333;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
user-select: none;
|
||||
&.active {
|
||||
transform: translateY(100%);
|
||||
> .btn {
|
||||
> i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
|
||||
> i {
|
||||
font-size: 1.4rem;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板和手机适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.part-selector-toolbar {
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
max-width: calc(100vw - 30px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.part-selector-toolbar {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.part-selector-toolbar.is-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toolbar-header {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
padding: 8px 0;
|
||||
// border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: relative;
|
||||
> .tip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
> span {
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
.header-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 3px 0;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
padding: 0 3rem 1.2rem;
|
||||
}
|
||||
|
||||
.tool-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tool-btn span {
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行4个按钮 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 - 每行3个按钮 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
.c-svg {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,744 +0,0 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="select-menu-panel"
|
||||
v-if="visible"
|
||||
:class="{ active: !closePanel }"
|
||||
>
|
||||
<div class="btn" @click="setClosePanel">
|
||||
<i class="fi fi-br-angle-left"></i>
|
||||
</div>
|
||||
<!-- 变换工具顶部 -->
|
||||
<div class="panel-select">
|
||||
<!-- <div class="panel-header">
|
||||
<div class="header-title">变换工具</div>
|
||||
</div> -->
|
||||
<!-- 分割线 -->
|
||||
<!-- <div class="panel-divider"></div> -->
|
||||
<!-- 变换工具内容 -->
|
||||
<div class="tool-content">
|
||||
<div
|
||||
class="object-item"
|
||||
v-for="v in activeObjects"
|
||||
:key="v.id"
|
||||
>
|
||||
<div class="title">{{ v.layer?.name }}</div>
|
||||
<div class="list">
|
||||
<div>
|
||||
<span class="label">W</span>
|
||||
<input
|
||||
type="number"
|
||||
:value="v.width"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">H</span>
|
||||
<input
|
||||
type="number"
|
||||
:value="v.height"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<span class="label">X</span>
|
||||
<input type="number" :value="v.left" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Y</span>
|
||||
<input type="number" :value="v.top" disabled />
|
||||
</div> -->
|
||||
<div>
|
||||
<span class="label iconfont icon-angle"></span>
|
||||
<input
|
||||
type="number"
|
||||
:value="Number(Number(v.angle).toFixed(3))"
|
||||
@change="(e) => changeAngle(e, v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="btn" @click="clickflipHorizontal(v)">
|
||||
<i class="iconfont icon-flip-horizontal"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.flipHorizontal") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn" @click="clickflipVertical(v)">
|
||||
<i class="iconfont icon-flip-vertical"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.flipVertical") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn" @click="clickCropImage(v)">
|
||||
<i class="iconfont icon-caijian"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.cropAndAdd") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import showViewVideo from "@/tool/mount";
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { loadImageUrlToLayer } from "../utils/imageHelper";
|
||||
import { TransformCommand } from "../commands/StateCommands";
|
||||
const props = defineProps({
|
||||
canvas: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commandManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layerManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
toolManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
//打开隐藏操作面板
|
||||
const closePanel = ref(false);
|
||||
const setClosePanel = () => {
|
||||
closePanel.value = !closePanel.value;
|
||||
};
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
setupCanvasListeners();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
removeCanvasListeners();
|
||||
});
|
||||
|
||||
// 监听 activeTool 变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
if (newTool === OperationType.SELECT) {
|
||||
show();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
if (activeObjects.value.length === 0) return;
|
||||
visible.value = true;
|
||||
closePanel.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
// 获取当前选中的对象
|
||||
const activeObjects = ref([]);
|
||||
const getActiveObject = (e) => {
|
||||
console.log("==========切换激活对象", e);
|
||||
activeObjects.value = e.selected.map((v) => v);
|
||||
activeObjects.value.forEach((v) => {
|
||||
v.layer = props.layerManager.getLayerById(v.layerId);
|
||||
});
|
||||
if (activeObjects.value.length === 0) {
|
||||
close();
|
||||
} else {
|
||||
show(false);
|
||||
}
|
||||
};
|
||||
const lastSelectLayerId = inject("lastSelectLayerId");
|
||||
const layers = inject("layers");
|
||||
const transformObject = (activeObj, initialState, finalState) => {
|
||||
const transformCmd = new TransformCommand({
|
||||
canvas: props.canvas,
|
||||
objectId: activeObj.id,
|
||||
initialState,
|
||||
finalState,
|
||||
objectType: activeObj.type,
|
||||
name: `变换 ${activeObj.type || "对象"}`,
|
||||
layerManager: props.layerManager,
|
||||
layers: layers,
|
||||
lastSelectLayerId: lastSelectLayerId,
|
||||
});
|
||||
props.layerManager.commandManager.execute(transformCmd, {
|
||||
name: "对象修改",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据左上角坐标计算旋转后的新坐标
|
||||
* @param {number} W - 宽度
|
||||
* @param {number} H - 高度
|
||||
* @param {number} currentX - 当前左上角x坐标
|
||||
* @param {number} currentY - 当前左上角y坐标
|
||||
* @param {number} currentAngleDeg - 当前角度(度)
|
||||
* @param {number} newAngleDeg - 新角度(度)
|
||||
* @returns {Object} 旋转后的左上角坐标 {x, y}
|
||||
*/
|
||||
function calculateRotatedTopLeftDeg(
|
||||
W,
|
||||
H,
|
||||
currentX,
|
||||
currentY,
|
||||
currentAngleDeg,
|
||||
newAngleDeg
|
||||
) {
|
||||
const currentAngle = (currentAngleDeg * Math.PI) / 180;
|
||||
const newAngle = (newAngleDeg * Math.PI) / 180;
|
||||
// 1. 用当前角度计算中心点位置
|
||||
const cosCurrent = Math.cos(currentAngle);
|
||||
const sinCurrent = Math.sin(currentAngle);
|
||||
const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent;
|
||||
const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent;
|
||||
|
||||
// 2. 用新角度计算旋转后的左上角位置
|
||||
const cosNew = Math.cos(newAngle);
|
||||
const sinNew = Math.sin(newAngle);
|
||||
const newX = Cx + (-W / 2) * cosNew - (-H / 2) * sinNew;
|
||||
const newY = Cy + (-W / 2) * sinNew + (-H / 2) * cosNew;
|
||||
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
// 改变角度
|
||||
const changeAngle = (e, obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = { ...initialState };
|
||||
const angle = e.target.value;
|
||||
if (obj.originX === "left" && obj.originY === "top") {
|
||||
const width = obj.width * obj.scaleX;
|
||||
const height = obj.height * obj.scaleY;
|
||||
const left = obj.left;
|
||||
const top = obj.top;
|
||||
const { x, y } = calculateRotatedTopLeftDeg(
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
obj.angle,
|
||||
angle
|
||||
);
|
||||
finalState.left = x;
|
||||
finalState.top = y;
|
||||
}
|
||||
finalState.angle = angle;
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
// 水平翻转
|
||||
const clickflipHorizontal = (obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = { ...initialState };
|
||||
finalState.flipX = !finalState.flipX;
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
// 垂直翻转
|
||||
const clickflipVertical = (obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = { ...initialState };
|
||||
finalState.flipY = !finalState.flipY;
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
// 裁剪图片
|
||||
const cropImage = inject("cropImage");
|
||||
const clickCropImage = async (obj) => {
|
||||
const base64 = await props.layerManager.getLayerToBase64(obj.layerId);
|
||||
if(base64) cropImage(base64).then((res) => {
|
||||
loadImageUrlToLayer({
|
||||
imageUrl: res,
|
||||
layerManager: props.layerManager,
|
||||
canvas: props.canvas,
|
||||
toolManager: props.toolManager,
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const updateActiveObjects = (arrs, keys) => {
|
||||
arrs.forEach((v) => {
|
||||
activeObjects.value.forEach((item) => {
|
||||
if (item.id === v.id) {
|
||||
keys.forEach((key) => (item[key] = v[key]));
|
||||
}
|
||||
});
|
||||
activeObjects.value = [...activeObjects.value];
|
||||
});
|
||||
};
|
||||
|
||||
const objectRotatingChange = (e) => {
|
||||
const arrs = [];
|
||||
if (e.target._objects) {
|
||||
e.target._objects.forEach((v) => arrs.push(v));
|
||||
} else {
|
||||
arrs.push(e.target);
|
||||
}
|
||||
updateActiveObjects(arrs, ["angle"]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置画布事件监听
|
||||
*/
|
||||
function setupCanvasListeners() {
|
||||
if (!props.canvas) return;
|
||||
// 鼠标事件
|
||||
props.canvas.on("selection:created", getActiveObject);
|
||||
props.canvas.on("selection:updated", getActiveObject);
|
||||
props.canvas.on("selection:cleared", close);
|
||||
props.canvas.on("object:rotating", objectRotatingChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除画布事件监听
|
||||
*/
|
||||
function removeCanvasListeners() {
|
||||
if (!props.canvas) return;
|
||||
|
||||
// 移除鼠标事件
|
||||
props.canvas.off("selection:created", getActiveObject);
|
||||
props.canvas.off("selection:updated", getActiveObject);
|
||||
props.canvas.off("selection:cleared", close);
|
||||
props.canvas.off("object:rotating", objectRotatingChange);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.select-menu-panel {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
max-width: min(90vw, 640px);
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
color: #333;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
user-select: none;
|
||||
&.active {
|
||||
transform: translateY(100%);
|
||||
> .btn {
|
||||
> i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .btn {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> i {
|
||||
font-size: 1.4rem;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板和手机适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.select-menu-panel {
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
max-width: calc(100vw - 30px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.select-menu-panel {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.select-menu-panel.is-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 8px 15px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.panel-select {
|
||||
// padding: 0 0 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.panel-header {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.panel-header {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tool-btn span {
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin: 0 10px 5px 10px;
|
||||
}
|
||||
|
||||
.tool-content {
|
||||
overflow-y: auto;
|
||||
max-height: 220px;
|
||||
margin-top: 10px;
|
||||
padding: 0 10px;
|
||||
> .object-item {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 10px 0;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
> .title {
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
> .list {
|
||||
display: flex;
|
||||
> div {
|
||||
margin-right: 15px;
|
||||
font-size: 14px;
|
||||
color: #474747;
|
||||
> .label {
|
||||
margin-right: 5px;
|
||||
}
|
||||
> input {
|
||||
width: 65px;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
> div.btn {
|
||||
position: relative;
|
||||
min-width: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
> .tip {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
margin-left: 0.8rem;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 97%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
> .tip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行4个按钮 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-content {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 - 每行3个按钮 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-content {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
.c-svg {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 280px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-dialog-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.feather-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.slider-control {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.slider-control::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #007aff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feather-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="repeat-setting">
|
||||
<div class="title">{{ t("Canvas.repeatSetting") }}</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.angle") }}</span>
|
||||
<angle-tool
|
||||
:angle="angle"
|
||||
@input="(e) => emit('inputFillAngle', e)"
|
||||
@change="(e) => emit('changeFillAngle', e)"
|
||||
style-type="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.scale") }}</span>
|
||||
<slider
|
||||
:min="1"
|
||||
:max="500"
|
||||
:step="1"
|
||||
is-input
|
||||
:tipFormatter="(v) => `${scale}%`"
|
||||
:value="scale"
|
||||
@input="inputFillScale"
|
||||
@change="changeFillScale"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">Gap X</span>
|
||||
<slider
|
||||
:min="0"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
is-input
|
||||
:tipFormatter="(v) => `${v}px`"
|
||||
:value="gapX"
|
||||
@input="(e) => emit('inputFill_Gap', e, gapY)"
|
||||
@change="(e) => emit('changeFill_Gap', e, gapY)"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">Gap Y</span>
|
||||
<slider
|
||||
:min="0"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
is-input
|
||||
:tipFormatter="(v) => `${v}px`"
|
||||
:value="gapY"
|
||||
@input="(e) => emit('inputFill_Gap', gapX, e)"
|
||||
@change="(e) => emit('changeFill_Gap', gapX, e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.offset") }}</span>
|
||||
<offset-tool
|
||||
:left="offsetX"
|
||||
:top="offsetY"
|
||||
@input="(e) => emit('inputFillOffset', e)"
|
||||
@change="(e) => emit('changeFillOffset', e)"
|
||||
:show-dish="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item offset">
|
||||
<offset-tool
|
||||
:left="offsetX"
|
||||
:top="offsetY"
|
||||
@input="(e) => emit('inputFillOffset', e)"
|
||||
@change="(e) => emit('changeFillOffset', e)"
|
||||
:show-input="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, computed } from "vue";
|
||||
import { getTransformScaleAngle } from "../../utils/helper";
|
||||
import AngleTool from "../tools/AngleTool.vue";
|
||||
import OffsetTool from "../tools/OffsetTool.vue";
|
||||
import Slider from "../tools/Slider.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
object: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
const angle = computed(
|
||||
() => getTransformScaleAngle(props.object.fill?.patternTransform).angle
|
||||
);
|
||||
const scale = computed(() => {
|
||||
const patternTransform = props.object.fill?.patternTransform;
|
||||
const scaleValue = getTransformScaleAngle(patternTransform).scale * 100;
|
||||
return Number(Number(scaleValue).toFixed(2));
|
||||
});
|
||||
const gapX = computed(() => props.object.fill_?.gapX || 0);
|
||||
const gapY = computed(() => props.object.fill_?.gapY || 0);
|
||||
const offsetX = computed(
|
||||
() => (props.object.fill?.offsetX / props.object.width) * 100
|
||||
);
|
||||
const offsetY = computed(
|
||||
() => (props.object.fill?.offsetY / props.object.height) * 100
|
||||
);
|
||||
const emit = defineEmits([
|
||||
"inputFillAngle",
|
||||
"changeFillAngle",
|
||||
"inputFillOffset",
|
||||
"changeFillOffset",
|
||||
"inputFillScale",
|
||||
"changeFillScale",
|
||||
"inputFill_Gap",
|
||||
"changeFill_Gap",
|
||||
]);
|
||||
const inputFillScale = (e) => {
|
||||
const scale = e / 100;
|
||||
emit("inputFillScale", scale);
|
||||
};
|
||||
const changeFillScale = (e) => {
|
||||
const scale = e / 100;
|
||||
emit("changeFillScale", scale);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.repeat-setting {
|
||||
user-select: none;
|
||||
width: 228px;
|
||||
> .title {
|
||||
line-height: 35px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: -12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
> .repeat-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.offset {
|
||||
justify-content: center;
|
||||
}
|
||||
> .label {
|
||||
min-width: 68px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&:not(.offset) > div {
|
||||
width: 120px;
|
||||
flex: 1;
|
||||
}
|
||||
> .slider {
|
||||
--slider-thumb-color1: #000;
|
||||
--slider-thumb-color2: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ref } from "vue";
|
||||
import i18n from "@/lang/index.ts";
|
||||
const { t } = i18n.global;
|
||||
|
||||
/** 填充重复模式 */
|
||||
export const getSelectOptions = () => ref([
|
||||
{ value: "no-repeat", label: t("Canvas.noRepeat") },
|
||||
{ value: "repeat", label: t("Canvas.repeat") },
|
||||
{ value: "repeat-x", label: t("Canvas.repeatX") },
|
||||
{ value: "repeat-y", label: t("Canvas.repeatY") },
|
||||
]);
|
||||
|
||||
/** 图层混合模式 */
|
||||
export const getLayerCompositeOptions = () => ref([
|
||||
{ value: "source-over", label: t("Canvas.CompositeNormal"), tip: t("Canvas.CompositeNormalTip") },// 正常
|
||||
{ value: "darken", label: t("Canvas.CompositeDarken"), tip: t("Canvas.CompositeDarkenTip") },// 变暗
|
||||
{ value: "multiply", label: t("Canvas.CompositeMultiply"), tip: t("Canvas.CompositeMultiplyTip") },// 正片叠底
|
||||
{ value: "color-burn", label: t("Canvas.CompositeColorBurn"), tip: t("Canvas.CompositeColorBurnTip") },// 颜色加深
|
||||
|
||||
{ value: "lighten", label: t("Canvas.CompositeLighten"), tip: t("Canvas.CompositeLightenTip") },// 颜色减淡
|
||||
{ value: "screen", label: t("Canvas.CompositeScreen"), tip: t("Canvas.CompositeScreenTip") },// 滤色
|
||||
{ value: "color-dodge", label: t("Canvas.CompositeColorDodge"), tip: t("Canvas.CompositeColorDodgeTip") },// 颜色减淡
|
||||
{ value: "lighter", label: t("Canvas.CompositeLighter"), tip: t("Canvas.CompositeLighterTip") },// 颜色减淡
|
||||
|
||||
{ value: "overlay", label: t("Canvas.CompositeOverlay"), tip: t("Canvas.CompositeOverlayTip") },// 叠加
|
||||
{ value: "soft-light", label: t("Canvas.CompositeSoftLight"), tip: t("Canvas.CompositeSoftLightTip") },// 柔光
|
||||
{ value: "hard-light", label: t("Canvas.CompositeHardLight"), tip: t("Canvas.CompositeHardLightTip") },// 强光
|
||||
|
||||
{ value: "difference", label: t("Canvas.CompositeDifference"), tip: t("Canvas.CompositeDifferenceTip") },// 差值
|
||||
{ value: "exclusion", label: t("Canvas.CompositeExclusion"), tip: t("Canvas.CompositeExclusionTip") },// 排除
|
||||
|
||||
{ value: "hue", label: t("Canvas.CompositeHue"), tip: t("Canvas.CompositeHueTip") },// 色相
|
||||
{ value: "saturation", label: t("Canvas.CompositeSaturation"), tip: t("Canvas.CompositeSaturationTip") },// 饱和度
|
||||
{ value: "color", label: t("Canvas.CompositeColor"), tip: t("Canvas.CompositeColorTip") },// 颜色
|
||||
{ value: "luminosity", label: t("Canvas.CompositeLuminosity"), tip: t("Canvas.CompositeLuminosityTip") },// 亮度
|
||||
|
||||
// { value: "destination-over", label: "背后", tip:"背后:新图形绘制到原内容下方" },
|
||||
// { value: "source-in", label: "颜色加深", tip:"颜色加深:只显示重叠部分,其他透明" },
|
||||
// { value: "destination-in", label: "颜色减淡", tip:"颜色减淡:只显示原内容与新图形重叠部分" },
|
||||
// { value: "source-out", label: "排除", tip:"排除:只显示新图形中不重叠部分" },
|
||||
// { value: "destination-out", label: "差值", tip:"差值:只清除原内容中与新图形重叠部分" },
|
||||
// { value: "xor", label: "排除", tip:"排除:重叠部分透明" },
|
||||
// { value: "copy", label: "正常", tip:"正常:完全忽略原内容,只显示新图形" },
|
||||
// { value: "source-atop", label: "叠加", tip:"叠加:只在与现有内容重叠处绘制新图形" },
|
||||
// { value: "destination-atop", label: "柔光", tip:"柔光:仅保留重叠部分,新图形在原内容后绘制" },
|
||||
// { value: "darker", label: "变暗", tip:"变暗:重叠部分颜色减淡" },
|
||||
]);
|
||||
@@ -0,0 +1,900 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="select-menu-panel"
|
||||
v-if="visible"
|
||||
:class="{ active: !closePanel }"
|
||||
>
|
||||
<div class="btn" @click="setClosePanel">
|
||||
<i class="fi fi-br-angle-left"></i>
|
||||
</div>
|
||||
<!-- 变换工具顶部 -->
|
||||
<div class="panel-select">
|
||||
<!-- <div class="panel-header">
|
||||
<div class="header-title">变换工具</div>
|
||||
</div> -->
|
||||
<!-- 分割线 -->
|
||||
<!-- <div class="panel-divider"></div> -->
|
||||
<!-- 变换工具内容 -->
|
||||
<div class="tool-content">
|
||||
<div
|
||||
class="object-item"
|
||||
v-for="v in activeObjects"
|
||||
:key="v.id"
|
||||
>
|
||||
<div class="title">{{ v.layer?.name }}</div>
|
||||
<div class="list">
|
||||
<div
|
||||
class="input"
|
||||
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||
>
|
||||
<angle-tool
|
||||
:angle="Number(Number(v.angle).toFixed(3))"
|
||||
@input="(e) => inputAngle(e, v)"
|
||||
@change="(e) => changeAngle(e, v)"
|
||||
/>
|
||||
</div>
|
||||
<div class="input">
|
||||
<span class="label"
|
||||
>{{ t("Canvas.opacity") }}:</span
|
||||
>
|
||||
<slider
|
||||
:tipFormatter="
|
||||
(v) => `${Math.round(v * 100)}%`
|
||||
"
|
||||
:value="v.opacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@change="(e) => changeOpacity(e, v)"
|
||||
@input="(e) => inputOpacity(e, v)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="btn"
|
||||
@click="clickflipHorizontal(v)"
|
||||
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||
>
|
||||
<i class="iconfont icon-flip-horizontal"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.flipHorizontal") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="btn"
|
||||
@click="clickflipVertical(v)"
|
||||
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||
>
|
||||
<i class="iconfont icon-flip-vertical"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.flipVertical") }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="btn"
|
||||
@click="clickCropImage(v)"
|
||||
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||
>
|
||||
<i class="iconfont icon-caijian"></i>
|
||||
<p class="tip">
|
||||
{{ t("Canvas.cropAndAdd") }}
|
||||
</p>
|
||||
</div> -->
|
||||
<!-- <div
|
||||
class="btn"
|
||||
@click="clickRasterizeLayer(v)"
|
||||
v-if="v.type !== 'image'"
|
||||
>
|
||||
<span class="label">{{ t("Canvas.RasterizedLayer") }}</span>
|
||||
</div> -->
|
||||
<div class="select">
|
||||
<!-- 混合模式 -->
|
||||
<i class="iconfont icon-hunhemoshi"></i>
|
||||
<my-select
|
||||
:defaultValue="
|
||||
v.layer?.blendMode ||
|
||||
v.globalCompositeOperation
|
||||
"
|
||||
:list="layerCompositeOptions"
|
||||
@change="
|
||||
(n, o) => setLayerComposite(n, o, v, 1)
|
||||
"
|
||||
@active="
|
||||
(n, o) => setLayerComposite(n, o, v, 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="btn"
|
||||
@click="clickTest(v)"
|
||||
>
|
||||
<span class="label">测试</span>
|
||||
</div> -->
|
||||
<div
|
||||
class="select"
|
||||
v-if="v.type === 'rect' || v.type === 'image'"
|
||||
>
|
||||
<!-- 平铺 -->
|
||||
<i class="iconfont icon-repeat"></i>
|
||||
<a-select
|
||||
size="small"
|
||||
:defaultValue="
|
||||
typeof v.fill === 'object'
|
||||
? v.fill?.repeat || 'no-repeat'
|
||||
: 'no-repeat'
|
||||
"
|
||||
:options="selectOptions"
|
||||
@change="(e) => changeFillRepeat(e, v)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 平铺设置 -->
|
||||
<a-popover
|
||||
v-if="v.type === 'rect'"
|
||||
trigger="click"
|
||||
destroyTooltipOnHide
|
||||
>
|
||||
<template #content>
|
||||
<repeat-setting
|
||||
:object="v"
|
||||
@inputFillAngle="
|
||||
(e) => inputFillAngle(e, v)
|
||||
"
|
||||
@changeFillAngle="
|
||||
(e) => changeFillAngle(e, v)
|
||||
"
|
||||
@inputFillOffset="
|
||||
(e) => inputFillOffset(e, v)
|
||||
"
|
||||
@changeFillOffset="
|
||||
(e) => changeFillOffset(e, v)
|
||||
"
|
||||
@inputFillScale="
|
||||
(e) => inputFillScale(e, v)
|
||||
"
|
||||
@changeFillScale="
|
||||
(e) => changeFillScale(e, v)
|
||||
"
|
||||
@inputFill_Gap="
|
||||
(x, y) => inputFill_Gap(x, y, v)
|
||||
"
|
||||
@changeFill_Gap="
|
||||
(x, y) => changeFill_Gap(x, y, v)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<div class="btn">
|
||||
<SvgIcon name="overallMore" size="18" />
|
||||
</div>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
import { OperationType, SpecialLayerId } from "../../utils/layerHelper";
|
||||
import { loadImageUrlToLayer } from "../../utils/imageHelper";
|
||||
import {
|
||||
calculateRotatedTopLeftDeg,
|
||||
createPatternTransform,
|
||||
getTransformScaleAngle,
|
||||
} from "../../utils/helper";
|
||||
import { TransformCommand } from "../../commands/StateCommands";
|
||||
import {
|
||||
FillRepeatCommand,
|
||||
FillRepeatChangeCommand,
|
||||
FillRepeatGapChangeCommand,
|
||||
} from "../../commands/FillRepeatCommand";
|
||||
import { SetLayerCompositeCommand } from "../../commands/LayerCommands.js";
|
||||
import RepeatSetting from "./RepeatSetting.vue";
|
||||
import Slider from "../tools/Slider.vue";
|
||||
import AngleTool from "../tools/AngleTool.vue";
|
||||
import MySelect from "../tools/MySelect.vue";
|
||||
import EventManager from "../../utils/event.js";
|
||||
import { getSelectOptions, getLayerCompositeOptions } from "./data.js";
|
||||
const selectOptions = getSelectOptions();
|
||||
const layerCompositeOptions = getLayerCompositeOptions();
|
||||
const props = defineProps({
|
||||
canvas: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commandManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layerManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
canvasManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
toolManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
//打开隐藏操作面板
|
||||
const closePanel = ref(false);
|
||||
const setClosePanel = () => {
|
||||
closePanel.value = !closePanel.value;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setupCanvasListeners();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
removeCanvasListeners();
|
||||
});
|
||||
|
||||
// 监听 activeTool 变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
if (newTool === OperationType.SELECT) {
|
||||
show();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
if (activeObjects.value.length === 0) return;
|
||||
visible.value = true;
|
||||
closePanel.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
// 获取当前选中的对象
|
||||
const activeObjects = ref([]);
|
||||
const getActiveObject = (e) => {
|
||||
console.log("==========切换激活对象", e, activeObjects);
|
||||
activeObjects.value = [...e.selected];
|
||||
activeObjects.value.forEach((v) => {
|
||||
v.layer = props.layerManager.getLayerById(v.layerId);
|
||||
});
|
||||
if (activeObjects.value.length === 0) {
|
||||
close();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
};
|
||||
//取消当前选中
|
||||
const cancelSelect = () => {
|
||||
activeObjects.value = [];
|
||||
close();
|
||||
};
|
||||
const lastSelectLayerId = inject("lastSelectLayerId");
|
||||
const layers = inject("layers");
|
||||
const transformObject = (
|
||||
activeObj,
|
||||
initialState,
|
||||
finalState,
|
||||
isCommand = true
|
||||
) => {
|
||||
const cmd = new TransformCommand({
|
||||
canvas: props.canvas,
|
||||
objectId: activeObj.id,
|
||||
initialState,
|
||||
finalState,
|
||||
objectType: activeObj.type,
|
||||
name: `变换 ${activeObj.type || "对象"}`,
|
||||
layerManager: props.layerManager,
|
||||
layers: layers,
|
||||
lastSelectLayerId: lastSelectLayerId,
|
||||
});
|
||||
if (isCommand) {
|
||||
props.commandManager.execute(cmd);
|
||||
} else {
|
||||
cmd.execute();
|
||||
}
|
||||
};
|
||||
// 改变不透明度
|
||||
const changeOpacity = (opacity, obj) => {
|
||||
props.layerManager?.setLayerOpacity(obj.layerId, opacity);
|
||||
};
|
||||
const inputOpacity = (opacity, obj) => {
|
||||
obj.opacity = opacity;
|
||||
props.canvas.renderAll();
|
||||
};
|
||||
// 改变角度
|
||||
const inputAngle = (angle, obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = computeAngleState(angle, obj, initialState);
|
||||
transformObject(obj, initialState, finalState, false);
|
||||
if (!obj.hasOwnProperty("oldState")) obj.oldState = initialState;
|
||||
};
|
||||
const changeAngle = (angle, obj) => {
|
||||
var initialState;
|
||||
if (obj.hasOwnProperty("oldState")) {
|
||||
initialState = obj.oldState;
|
||||
delete obj.oldState;
|
||||
} else {
|
||||
initialState = TransformCommand.captureTransformState(obj);
|
||||
}
|
||||
const finalState = computeAngleState(angle, obj, initialState);
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
const computeAngleState = (angle, obj, initialState) => {
|
||||
const finalState = { ...initialState };
|
||||
if (obj.originX === "left" && obj.originY === "top") {
|
||||
const width = obj.width * obj.scaleX;
|
||||
const height = obj.height * obj.scaleY;
|
||||
const left = obj.left;
|
||||
const top = obj.top;
|
||||
const { x, y } = calculateRotatedTopLeftDeg(
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top,
|
||||
obj.angle,
|
||||
angle
|
||||
);
|
||||
finalState.left = x;
|
||||
finalState.top = y;
|
||||
}
|
||||
finalState.angle = angle;
|
||||
return finalState;
|
||||
};
|
||||
|
||||
// 水平翻转
|
||||
const clickflipHorizontal = (obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = { ...initialState };
|
||||
finalState.flipX = !finalState.flipX;
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
// 垂直翻转
|
||||
const clickflipVertical = (obj) => {
|
||||
const initialState = TransformCommand.captureTransformState(obj);
|
||||
const finalState = { ...initialState };
|
||||
finalState.flipY = !finalState.flipY;
|
||||
transformObject(obj, initialState, finalState);
|
||||
};
|
||||
// 裁剪图片
|
||||
const cropImage = inject("cropImage");
|
||||
const clickCropImage = async (obj) => {
|
||||
const base64 = await props.layerManager.getLayerToBase64(obj.layerId);
|
||||
if (base64)
|
||||
cropImage(base64).then((res) => {
|
||||
loadImageUrlToLayer({
|
||||
imageUrl: res,
|
||||
layerManager: props.layerManager,
|
||||
canvas: props.canvas,
|
||||
toolManager: props.toolManager,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 栅格化图层
|
||||
const clickRasterizeLayer = (obj) => {
|
||||
props.layerManager.rasterizeLayer(obj.layerId);
|
||||
};
|
||||
|
||||
// 改变填充重复
|
||||
const changeFillRepeat = async (value, obj) => {
|
||||
console.log("==========改变填充重复", obj.type);
|
||||
const cmd = new FillRepeatCommand({
|
||||
canvas: props.canvas,
|
||||
layers: layers,
|
||||
canvasManager: props.canvasManager,
|
||||
layerManager: props.layerManager,
|
||||
layerId: obj.layerId,
|
||||
fillRepeat: value,
|
||||
});
|
||||
props.commandManager.execute(cmd);
|
||||
};
|
||||
// 改变填充角度
|
||||
const inputFillAngle = (angle, obj) => {
|
||||
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||
const fill = obj.get("fill");
|
||||
const scale = getTransformScaleAngle(fill?.patternTransform).scale;
|
||||
const pattern = new fabric.Pattern({
|
||||
...fill,
|
||||
patternTransform: createPatternTransform(scale, angle),
|
||||
});
|
||||
obj.set("fill", pattern);
|
||||
props.canvas.renderAll();
|
||||
};
|
||||
const changeFillAngle = (angle, obj) => {
|
||||
const fill = obj.get("fill");
|
||||
const scale = getTransformScaleAngle(fill?.patternTransform).scale;
|
||||
const pattern = {
|
||||
patternTransform: createPatternTransform(scale, angle),
|
||||
};
|
||||
changeFill(obj, pattern);
|
||||
};
|
||||
// 改变填充偏移
|
||||
const inputFillOffset = (value, obj) => {
|
||||
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||
const pattern = new fabric.Pattern({
|
||||
...obj.get("fill"),
|
||||
offsetX: (value.left / 100) * obj.width,
|
||||
offsetY: (value.top / 100) * obj.height,
|
||||
});
|
||||
obj.set("fill", pattern);
|
||||
props.canvas.renderAll();
|
||||
};
|
||||
const changeFillOffset = (value, obj) => {
|
||||
const pattern = new fabric.Pattern({
|
||||
offsetX: (value.left / 100) * obj.width,
|
||||
offsetY: (value.top / 100) * obj.height,
|
||||
});
|
||||
changeFill(obj, pattern);
|
||||
};
|
||||
// 改变填充缩放
|
||||
const inputFillScale = (scale, obj) => {
|
||||
if (!obj.oldPattern) obj.oldPattern = obj.get("fill");
|
||||
const fill = obj.get("fill");
|
||||
const angle = getTransformScaleAngle(fill?.patternTransform).angle;
|
||||
const pattern = new fabric.Pattern({
|
||||
...fill,
|
||||
patternTransform: createPatternTransform(scale, angle),
|
||||
});
|
||||
obj.set("fill", pattern);
|
||||
props.canvas.renderAll();
|
||||
};
|
||||
const changeFillScale = (scale, obj) => {
|
||||
const fill = obj.get("fill");
|
||||
const angle = getTransformScaleAngle(fill?.patternTransform).angle;
|
||||
const pattern = {
|
||||
patternTransform: createPatternTransform(scale, angle),
|
||||
};
|
||||
changeFill(obj, pattern);
|
||||
};
|
||||
const changeFill = (obj, pattern) => {
|
||||
const cmd = new FillRepeatChangeCommand({
|
||||
canvas: props.canvas,
|
||||
layers: layers,
|
||||
canvasManager: props.canvasManager,
|
||||
layerManager: props.layerManager,
|
||||
layerId: obj.layerId,
|
||||
newPattern: pattern,
|
||||
});
|
||||
props.commandManager.execute(cmd);
|
||||
};
|
||||
// 改变填充间隙
|
||||
const inputFill_Gap = (gapX, gapY, obj) => {
|
||||
const cmd = new FillRepeatGapChangeCommand({
|
||||
canvas: props.canvas,
|
||||
layers: layers,
|
||||
canvasManager: props.canvasManager,
|
||||
layerManager: props.layerManager,
|
||||
layerId: obj.layerId,
|
||||
newGapX: gapX,
|
||||
newGapY: gapY,
|
||||
record: true,
|
||||
});
|
||||
cmd.execute();
|
||||
};
|
||||
const changeFill_Gap = (gapX, gapY, obj) => {
|
||||
if (obj.oldFill_) {
|
||||
obj.fill_ = { ...obj.oldFill_ };
|
||||
delete obj.oldFill_;
|
||||
}
|
||||
const cmd = new FillRepeatGapChangeCommand({
|
||||
canvas: props.canvas,
|
||||
layers: layers,
|
||||
canvasManager: props.canvasManager,
|
||||
layerManager: props.layerManager,
|
||||
layerId: obj.layerId,
|
||||
newGapX: gapX,
|
||||
newGapY: gapY,
|
||||
});
|
||||
props.commandManager.execute(cmd);
|
||||
};
|
||||
|
||||
const setLayerComposite = (newValue, oldValue, obj, isCmd) => {
|
||||
const cmd = new SetLayerCompositeCommand({
|
||||
canvas: props.canvas,
|
||||
layers: layers,
|
||||
layerManager: props.layerManager,
|
||||
layerId: obj.layerId,
|
||||
newValue: newValue,
|
||||
oldValue: oldValue,
|
||||
});
|
||||
if (isCmd) {
|
||||
props.commandManager.execute(cmd);
|
||||
} else {
|
||||
cmd.execute();
|
||||
}
|
||||
};
|
||||
|
||||
const clickTest = (obj) => {
|
||||
console.log("==========点击测试", obj);
|
||||
};
|
||||
|
||||
// 更新选中对象属性
|
||||
const updateActiveObjects = (arrs, keys, isNumber = true) => {
|
||||
arrs.forEach((v) => {
|
||||
activeObjects.value.forEach((item) => {
|
||||
if (item.id === v.id) {
|
||||
keys.forEach(
|
||||
(key) => (item[key] = isNumber ? Number(v[key]) : v[key])
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
activeObjects.value = [...activeObjects.value];
|
||||
};
|
||||
|
||||
// 旋转对象时更新角度
|
||||
const objectRotatingChange = (e) => {
|
||||
const arrs = [];
|
||||
if (e.target._objects) {
|
||||
e.target._objects.forEach((v) => arrs.push(v));
|
||||
} else {
|
||||
arrs.push(e.target);
|
||||
}
|
||||
updateActiveObjects(arrs, ["angle"]);
|
||||
};
|
||||
|
||||
// 对象属性修改后触发
|
||||
const objectModifiedChange = (e) => {
|
||||
console.log("==========object:modified", e.target);
|
||||
};
|
||||
// 不透明度撤销时触发
|
||||
const objectOpacityUndo = (layerId, opacity) => {
|
||||
const layerObjects = props.canvas
|
||||
.getObjects()
|
||||
.filter((obj) => obj.layerId === layerId);
|
||||
updateActiveObjects(layerObjects, ["opacity"]);
|
||||
};
|
||||
// 对象属性修改撤销时触发
|
||||
const objectModifiedUndo = (object) => {
|
||||
updateActiveObjects([object], ["angle"]);
|
||||
};
|
||||
// 组合操作撤销时触发
|
||||
const objectCompositeChange = (object) => {
|
||||
updateActiveObjects([object], ["globalCompositeOperation"], false);
|
||||
};
|
||||
/**
|
||||
* 设置画布事件监听
|
||||
*/
|
||||
function setupCanvasListeners() {
|
||||
if (!props.canvas) return;
|
||||
// 注册事件
|
||||
props.canvas.on("selection:created", getActiveObject);
|
||||
props.canvas.on("selection:updated", getActiveObject);
|
||||
props.canvas.on("selection:cleared", cancelSelect);
|
||||
props.canvas.on("object:rotating", objectRotatingChange);
|
||||
props.canvas.on("object:modified", objectModifiedChange);
|
||||
EventManager.on("object:opacity:execute", objectOpacityUndo);
|
||||
EventManager.on("object:opacity:undo", objectOpacityUndo);
|
||||
EventManager.on("object:modified:execute", objectModifiedUndo);
|
||||
EventManager.on("object:modified:undo", objectModifiedUndo);
|
||||
EventManager.on("object:composite:execute", objectCompositeChange);
|
||||
EventManager.on("object:composite:undo", objectCompositeChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除画布事件监听
|
||||
*/
|
||||
function removeCanvasListeners() {
|
||||
if (!props.canvas) return;
|
||||
// 移除事件
|
||||
props.canvas.off("selection:created", getActiveObject);
|
||||
props.canvas.off("selection:updated", getActiveObject);
|
||||
props.canvas.off("selection:cleared", cancelSelect);
|
||||
props.canvas.off("object:rotating", objectRotatingChange);
|
||||
props.canvas.off("object:modified", objectModifiedChange);
|
||||
EventManager.off("object:opacity:execute", objectOpacityUndo);
|
||||
EventManager.off("object:opacity:undo", objectOpacityUndo);
|
||||
EventManager.off("object:modified:execute", objectModifiedUndo);
|
||||
EventManager.off("object:modified:undo", objectModifiedUndo);
|
||||
EventManager.off("object:composite:execute", objectCompositeChange);
|
||||
EventManager.off("object:composite:undo", objectCompositeChange);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.select-menu-panel {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
// max-width: min(90vw, 640px);
|
||||
max-width: 95%;
|
||||
width: 80rem;
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
color: #333;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
user-select: none;
|
||||
&.active {
|
||||
transform: translateY(100%);
|
||||
> .btn {
|
||||
> i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .btn {
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> i {
|
||||
font-size: 1.4rem;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板和手机适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.select-menu-panel {
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
max-width: calc(100vw - 30px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.select-menu-panel {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.select-menu-panel.is-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 8px 15px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.panel-select {
|
||||
// padding: 0 0 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.panel-header {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.panel-header {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tool-btn span {
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin: 0 10px 5px 10px;
|
||||
}
|
||||
|
||||
.tool-content {
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0 1.5rem;
|
||||
> .object-item {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 1rem 0;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
> .title {
|
||||
text-align: left;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
> .list {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1.5rem;
|
||||
position: relative;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
> .iconfont {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
> .label {
|
||||
font-size: 1.3rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
> .angle-tool {
|
||||
width: 9rem;
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
// margin-left: 0.8rem;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 97%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
> .tip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
> div.input {
|
||||
font-size: 1.4rem;
|
||||
color: #474747;
|
||||
> .label {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
> .iconfont {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
> .slider {
|
||||
width: 8rem;
|
||||
}
|
||||
}
|
||||
> div.select {
|
||||
> .iconfont {
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .my-select,
|
||||
> .ant-select {
|
||||
width: 12rem;
|
||||
text-align: left;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
> div.btn {
|
||||
min-width: 2.8rem;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
> div.color {
|
||||
width: 4rem;
|
||||
height: 2.5rem;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to bottom, #ff0000, #ffff00);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行4个按钮 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-content {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 - 每行3个按钮 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-content {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,8 +29,12 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
clothingMinIOPath: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL-线稿
|
||||
},
|
||||
});
|
||||
|
||||
console.log(props.clothingMinIOPath)
|
||||
const commandManager = inject("commandManager");
|
||||
const layerManager = inject("layerManager"); // 图层管理器
|
||||
|
||||
@@ -166,6 +170,19 @@ const normalToolsList = ref([
|
||||
icon: { name: "CFont", size: "20" },
|
||||
class: "text-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.PART,
|
||||
title: t("Canvas.GarmentPartSelector"),
|
||||
action: () => selectTool(OperationType.PART),
|
||||
icon: { name: "CPart", size: "28" },
|
||||
class: "part-btn",
|
||||
activeList: [
|
||||
OperationType.PART,
|
||||
OperationType.PART_RECTANGLE,
|
||||
OperationType.PART_BRUSH,
|
||||
OperationType.PART_ERASER,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
title: t("Canvas.help"),
|
||||
@@ -236,7 +253,13 @@ const redGreenToolsList = ref([
|
||||
|
||||
// 根据模式选择工具列表
|
||||
const toolsList = computed(() => {
|
||||
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
||||
const list = props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
||||
return list.filter(tool => {
|
||||
if(tool.id === OperationType.PART){
|
||||
return !!props.clothingMinIOPath;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function selectTool(tool, isRedGreenMode = false) {
|
||||
@@ -412,8 +435,12 @@ const handleToolClick = (tool) => {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.tools-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.red-green-mode {
|
||||
background-color: #fff4f4;
|
||||
background-color: #060505;
|
||||
}
|
||||
|
||||
.mode-indicator {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
z-index: 6;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.layers-header {
|
||||
@@ -132,10 +132,11 @@
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.layers-list-container {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.layers-list {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.layer-item {
|
||||
position: relative;
|
||||
@@ -270,6 +271,13 @@
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.layer-color-btn {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
.layer-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
|
||||
167
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="angle-tool" :disabled="disabled">
|
||||
<template v-if="styleType === '1'">
|
||||
<div
|
||||
ref="dishRef"
|
||||
class="dish"
|
||||
@mousedown.stop="mousedown"
|
||||
@touchmove.stop="mousedown"
|
||||
>
|
||||
<div
|
||||
class="pointer"
|
||||
:style="{ transform: `rotate(${angle}deg)` }"
|
||||
>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input
|
||||
type="number"
|
||||
v-model="angle"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<my-input
|
||||
v-if="styleType === '2'"
|
||||
v-model="angle"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
after="°"
|
||||
icon="icon-angle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import { calculateAngle } from "../../utils/helper";
|
||||
import MyInput from "./MyInput.vue";
|
||||
// Props
|
||||
const props = defineProps({
|
||||
styleType: {
|
||||
type: String,
|
||||
default: "1",
|
||||
},
|
||||
angle: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const angle = ref(props.angle);
|
||||
watch(
|
||||
() => props.angle,
|
||||
(value) => {
|
||||
angle.value = value;
|
||||
}
|
||||
);
|
||||
const dishRef = ref<HTMLDivElement>();
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
if (props.disabled) return;
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const { left, top, width, height } =
|
||||
dishRef.value.getBoundingClientRect();
|
||||
const centerX = left + width / 2;
|
||||
const centerY = top + height / 2;
|
||||
const { clientX, clientY } = e?.touches?.[0] || e;
|
||||
angle.value = calculateAngle(centerX, centerY, clientX, clientY, true);
|
||||
onInput();
|
||||
};
|
||||
mousemove(e);
|
||||
const mouseup = () => {
|
||||
onChange();
|
||||
document.removeEventListener("mousemove", mousemove);
|
||||
document.removeEventListener("touchmove", mousemove);
|
||||
document.removeEventListener("mouseup", mouseup);
|
||||
document.removeEventListener("touchend", mouseup);
|
||||
};
|
||||
document.addEventListener("mousemove", mousemove);
|
||||
document.addEventListener("touchmove", mousemove);
|
||||
document.addEventListener("mouseup", mouseup);
|
||||
document.addEventListener("touchend", mouseup);
|
||||
};
|
||||
const onInput = () => !props.disabled && emit("input", angle.value);
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
if (props.disabled) return;
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", angle.value), 500);
|
||||
};
|
||||
// var angleTime = null;
|
||||
// watch(angle, (value) => {
|
||||
// emit("input", value);
|
||||
// clearTimeout(angleTime);
|
||||
// angleTime = setTimeout(() => emit("change", value), 50);
|
||||
// });
|
||||
// defineExpose({
|
||||
// open,
|
||||
// close,
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.angle-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
--color: #000;
|
||||
&[disabled="true"] {
|
||||
--color: #b2b2b2;
|
||||
> .dish {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
> .dish {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
> .pointer {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 35%;
|
||||
height: 35%;
|
||||
background-color: var(--color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .input {
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
color: var(--color);
|
||||
flex: 1;
|
||||
// min-width: 45px;
|
||||
// max-width: 80px;
|
||||
// width: 50px;
|
||||
> input {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
> .my-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="my-input">
|
||||
<span class="decorate"></span>
|
||||
<span v-show="icon" :class="['iconfont', icon]"></span>
|
||||
<span v-show="before" class="before">{{ before }}</span>
|
||||
<input v-bind="$attrs" :value="modelValue" @input="onInput" />
|
||||
<span v-show="after" class="after">{{ after }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
modelValue: { type: Number, default: 0 },
|
||||
icon: { default: "", type: String },
|
||||
before: { default: "", type: String },
|
||||
after: { default: "", type: String },
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "input"]);
|
||||
const onInput = (e) => {
|
||||
const value = e.target.value;
|
||||
emit("update:modelValue", value);
|
||||
emit("input", value);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.my-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 20px;
|
||||
padding: 0 4px 0 2px;
|
||||
> .decorate {
|
||||
width: 2px;
|
||||
background-color: rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 85%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .iconfont {
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .before {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .after {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
> input {
|
||||
font-size: 12px;
|
||||
width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<a-select
|
||||
class="my-select"
|
||||
:size="size"
|
||||
@change="change"
|
||||
:defaultValue="defaultValue"
|
||||
@dropdownVisibleChange="dropdownVisibleChange"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="v in list"
|
||||
:key="v.value"
|
||||
:value="v.value"
|
||||
:title="v.tip"
|
||||
@mouseover.stop.prevent="mouseover(v)"
|
||||
@mouseleave="mouseleave(v)"
|
||||
>{{ v.label }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
defaultValue: {
|
||||
default: "",
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "small",
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "active"]);
|
||||
const isChange = ref(false);
|
||||
const initValue = ref(props.defaultValue);
|
||||
const activeValue = ref(props.defaultValue);
|
||||
const timeout = ref(null);
|
||||
const mouseover = (v) => {
|
||||
clearTimeout(timeout.value);
|
||||
if (v.value === activeValue.value) return;
|
||||
emit("active", v.value, activeValue.value);
|
||||
activeValue.value = v.value;
|
||||
};
|
||||
const mouseleave = () => {
|
||||
clearTimeout(timeout.value);
|
||||
timeout.value = setTimeout(() => {
|
||||
dropdownVisibleChange(false);
|
||||
}, 100);
|
||||
};
|
||||
const change = (v) => {
|
||||
isChange.value = true;
|
||||
emit("change", v, initValue.value);
|
||||
};
|
||||
const dropdownVisibleChange = (v) => {
|
||||
if (v) {
|
||||
isChange.value = false;
|
||||
initValue.value = props.defaultValue;
|
||||
} else if (!isChange.value) {
|
||||
emit("active", initValue.value, activeValue.value);
|
||||
activeValue.value = initValue.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="offset-tool">
|
||||
<div class="input" v-show="showInput">
|
||||
<my-input
|
||||
v-model="left"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
type="number"
|
||||
before="X"
|
||||
after="%"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
<my-input
|
||||
v-model="top"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
type="number"
|
||||
before="Y"
|
||||
after="%"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="dish"
|
||||
@mousedown="mousedown"
|
||||
@touchstart="mousedown"
|
||||
ref="dishRef"
|
||||
v-show="showDish"
|
||||
>
|
||||
<img src="/src/assets/images/icon/xyz.png" />
|
||||
<span class="ball" :style="ballStyle"></span>
|
||||
<span class="tip x">X: {{ left }}%</span>
|
||||
<span class="tip y">Y: {{ top }}%</span>
|
||||
<span class="line x"></span>
|
||||
<span class="line y"></span>
|
||||
<span class="line z" :style="lineZStyle"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
||||
import MyInput from "./MyInput.vue";
|
||||
const props = defineProps({
|
||||
left: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDish: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
// 工具的实际坐标 -100 ~ 100
|
||||
const top = ref(Math.round(props.top));
|
||||
const left = ref(Math.round(props.left));
|
||||
|
||||
// 原点的坐标 0 ~ 100
|
||||
const ballStyle = computed(() => ({
|
||||
top: 50 + Number(top.value) / 2 + "%",
|
||||
left: 50 + Number(left.value) / 2 + "%",
|
||||
}));
|
||||
watch(
|
||||
() => props.left,
|
||||
(v) => (left.value = Math.round(v))
|
||||
);
|
||||
watch(
|
||||
() => props.top,
|
||||
(v) => (top.value = Math.round(v))
|
||||
);
|
||||
const dishRef = ref<HTMLDivElement>();
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const rect = dishRef.value.getBoundingClientRect();
|
||||
const X = e.clientX || (e as TouchEvent).touches[0].clientX;
|
||||
const Y = e.clientY || (e as TouchEvent).touches[0].clientY;
|
||||
var x = ((X - rect.left) / rect.width) * 100;
|
||||
var y = ((Y - rect.top) / rect.height) * 100;
|
||||
if (x < 0) x = 0;
|
||||
if (x > 100) x = 100;
|
||||
if (y < 0) y = 0;
|
||||
if (y > 100) y = 100;
|
||||
left.value = Math.round((x - 50) * 2);
|
||||
top.value = Math.round((y - 50) * 2);
|
||||
onInput();
|
||||
};
|
||||
mousemove(e);
|
||||
const mouseup = () => {
|
||||
onChange();
|
||||
document.removeEventListener("mousemove", mousemove);
|
||||
document.removeEventListener("touchmove", mousemove);
|
||||
document.removeEventListener("mouseup", mouseup);
|
||||
document.removeEventListener("touchend", mouseup);
|
||||
};
|
||||
document.addEventListener("mousemove", mousemove);
|
||||
document.addEventListener("touchmove", mousemove);
|
||||
document.addEventListener("mouseup", mouseup);
|
||||
document.addEventListener("touchend", mouseup);
|
||||
};
|
||||
const onInput = () => {
|
||||
emit("input", { left: left.value, top: top.value });
|
||||
};
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => {
|
||||
emit("change", {
|
||||
left: left.value,
|
||||
top: top.value,
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
const lineZStyle = computed(() => ({
|
||||
"--rotateZ": calculateAngle(0, 0, left.value, top.value) + "deg",
|
||||
width: calculateDistance(0, 0, left.value, top.value) / 2 + "%",
|
||||
}));
|
||||
// 计算角度
|
||||
function calculateAngle(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y1 - y2;
|
||||
let angle = Math.atan2(deltaX, deltaY) * (180 / Math.PI) - 90;
|
||||
return angle;
|
||||
}
|
||||
// 计算距离
|
||||
function calculateDistance(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
return distance;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.offset-tool {
|
||||
position: relative;
|
||||
> .input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> * {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .dish {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: #f6f6f6;
|
||||
margin-top: 24px;
|
||||
> * {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
> .ball {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid #fff;
|
||||
background-color: #333;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0.68px 1.7px 0px rgba(0, 0, 0, 0.26);
|
||||
}
|
||||
> .tip {
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
line-height: 24px;
|
||||
&.x {
|
||||
top: 50%;
|
||||
right: 0%;
|
||||
transform: translate(100%, -50%);
|
||||
padding-left: 6px;
|
||||
}
|
||||
&.y {
|
||||
top: 0%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
}
|
||||
> .line {
|
||||
border-color: #d9d9d9;
|
||||
border-style: dashed;
|
||||
border-width: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
&.x {
|
||||
width: 100%;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
&.y {
|
||||
height: 100%;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
&.z {
|
||||
width: 50%;
|
||||
border-top-width: 1px;
|
||||
border-color: #454754;
|
||||
transform: translate(0%, -50%) rotateZ(var(--rotateZ));
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
173
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="slider" :disabled="disabled">
|
||||
<div
|
||||
class="input-range"
|
||||
:style="{
|
||||
'--progress': (value - props.min) / (props.max - props.min),
|
||||
}"
|
||||
>
|
||||
<span class="tip">{{ props.tipFormatter(value) }}</span>
|
||||
<input
|
||||
type="range"
|
||||
v-model="value"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:step="props.step"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="input" v-show="isInput">
|
||||
<my-input
|
||||
v-model="value"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
:step="props.step"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import MyInput from "./MyInput.vue";
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
tipFormatter: {
|
||||
type: Function,
|
||||
default: (v) => v,
|
||||
},
|
||||
isInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const value = ref(props.value);
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => (value.value = v)
|
||||
);
|
||||
const onInput = () => !props.disabled && emit("input", Number(value.value));
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
if (props.disabled) return;
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", Number(value.value)), 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.slider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
--input-thumb-size: 10px;
|
||||
--backcolor1: var(--slider-thumb-color1, #4285f4);
|
||||
--backcolor2: var(--slider-thumb-color2, rgba(0, 0, 0, 0.1));
|
||||
&:hover {
|
||||
> .input-range > .tip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
> .input-range {
|
||||
position: relative;
|
||||
flex: 2;
|
||||
> input {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--backcolor1) 0%,
|
||||
var(--backcolor1) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) 100%
|
||||
);
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: var(--input-thumb-size);
|
||||
height: var(--input-thumb-size);
|
||||
border-radius: 50%;
|
||||
background: var(--backcolor1); /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #666;
|
||||
top: 0;
|
||||
left: calc(
|
||||
(100% - var(--input-thumb-size)) * var(--progress) +
|
||||
var(--input-thumb-size) / 2
|
||||
);
|
||||
transform: translate(-50%, -100%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 97%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .input {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
> input {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,6 +17,7 @@ import { KeyboardManager } from "./managers/events/KeyboardManager.js";
|
||||
import CanvasConfig from "./config/canvasConfig.js";
|
||||
import { LiquifyManager } from "./managers/liquify/LiquifyManager";
|
||||
import { SelectionManager } from "./managers/selection/SelectionManager";
|
||||
import { PartManager } from "./managers/PartManager";
|
||||
import { RedGreenModeManager } from "./managers/RedGreenModeManager";
|
||||
import texturePresetManager from "./managers/brushes/TexturePresetManager";
|
||||
import { BrushStore } from "./store/BrushStore";
|
||||
@@ -35,8 +36,10 @@ import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
||||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||
import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
|
||||
import PalletPanel from "./components/PalletPanel/index.vue";
|
||||
import SelectMenuPanel from "./components/SelectMenuPanel/index.vue"; // 引入选择工具菜单组件
|
||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||
import PartSelectorPanel from "./components/PartSelectorPanel.vue"; // 引入部件选取面板
|
||||
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
||||
import { ToolManager } from "./managers/ToolManager.js";
|
||||
import { fabric } from "fabric-with-all";
|
||||
@@ -45,6 +48,7 @@ import {
|
||||
loadImageUrlToLayer,
|
||||
loadImage,
|
||||
} from "./utils/imageHelper.js";
|
||||
import { optimizeCanvasRendering } from "./utils/helper";
|
||||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
@@ -56,7 +60,9 @@ const emit = defineEmits([
|
||||
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
|
||||
"changeCanvas", // 画布变更事件
|
||||
"canvasInit", // 画布初始化事件
|
||||
"canvas-load-json-success", // 画布加载JSON成功事件
|
||||
"trigger-library", // 触发打开Library选择图片事件
|
||||
"before-unmount-export-extra-info", // 组件卸载前导出额外信息事件
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
@@ -64,6 +70,10 @@ const props = defineProps({
|
||||
type: [Object, String],
|
||||
default: "", // 默认空
|
||||
},
|
||||
otherData: {
|
||||
type: [Object, null],
|
||||
default: null, // 默认空对象
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => CanvasConfig, // 默认配置
|
||||
@@ -76,9 +86,17 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false, // 是否启用红绿图模式
|
||||
},
|
||||
clothingMinIOPath: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL-线稿
|
||||
},
|
||||
clothingImageUrl: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL
|
||||
default: "", // 衣服底图URL-线稿
|
||||
},
|
||||
clothingImageUrl2: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL-上色
|
||||
},
|
||||
redGreenImageUrl: {
|
||||
type: String,
|
||||
@@ -172,6 +190,7 @@ let keyboardManager = null;
|
||||
let toolManager = null;
|
||||
let liquifyManager = null;
|
||||
let selectionManager = null;
|
||||
let partManager = null;
|
||||
let redGreenModeManager = null;
|
||||
|
||||
// 快捷键帮助模态框状态
|
||||
@@ -213,6 +232,7 @@ function handleCanvasInit(isLoadJson = false) {
|
||||
keyboardManager,
|
||||
liquifyManager,
|
||||
selectionManager,
|
||||
partManager,
|
||||
redGreenModeManager,
|
||||
});
|
||||
}
|
||||
@@ -250,6 +270,8 @@ onMounted(async () => {
|
||||
canvasColor,
|
||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||
isFixedErasable: props.isFixedErasable,
|
||||
props,
|
||||
emit,
|
||||
});
|
||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||
canvasManager.activeLayerId = activeLayerId;
|
||||
@@ -307,6 +329,7 @@ onMounted(async () => {
|
||||
canvas: canvasManager.canvas,
|
||||
commandManager,
|
||||
layerManager,
|
||||
canvasManager,
|
||||
toolManager,
|
||||
isRedGreenMode,
|
||||
pasteText: (text) => {
|
||||
@@ -359,9 +382,20 @@ onMounted(async () => {
|
||||
selectionManager = new SelectionManager({
|
||||
canvas: canvasManager.canvas,
|
||||
layerManager,
|
||||
props,
|
||||
});
|
||||
canvasManager.setSelectionManager(selectionManager);
|
||||
|
||||
// 初始化部件选择管理器
|
||||
partManager = new PartManager({
|
||||
canvas: canvasManager.canvas,
|
||||
layerManager,
|
||||
canvasManager,
|
||||
toolManager,
|
||||
props,
|
||||
});
|
||||
canvasManager.setPartManager(partManager);
|
||||
|
||||
if (props.canvasJSON) {
|
||||
// 如果传入了初始JSON数据,加载到画布上
|
||||
if (typeof props.canvasJSON === "string") {
|
||||
@@ -435,6 +469,12 @@ onMounted(async () => {
|
||||
canvasManager.canvas.width,
|
||||
canvasManager.canvas.height
|
||||
);
|
||||
|
||||
if(props.otherData && !props.otherData.canvasId) {
|
||||
await canvasManager?.createOtherLayers(props.otherData);
|
||||
await layerManager?.layerSort?.rearrangeObjects();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// // 设置固定图层是否可擦除
|
||||
@@ -464,38 +504,13 @@ onMounted(async () => {
|
||||
}, 700);
|
||||
});
|
||||
|
||||
let throttleTimeout = null;
|
||||
let lastRunTime = 0;
|
||||
let trailingTimeout = null;
|
||||
|
||||
let throttleDelay = 100;
|
||||
observer = new ResizeObserver((entries) => {
|
||||
const now = Date.now();
|
||||
const throttleDelay = 100;
|
||||
|
||||
if (!throttleTimeout) {
|
||||
// 立即执行一次
|
||||
handleWindowResize();
|
||||
layerManager?.updateLayersObjectsInteractivity?.();
|
||||
setTimeout(() => {
|
||||
layerManager?.updateLayersObjectsInteractivity?.();
|
||||
});
|
||||
lastRunTime = now;
|
||||
|
||||
throttleTimeout = setTimeout(() => {
|
||||
throttleTimeout = null;
|
||||
}, throttleDelay);
|
||||
} else {
|
||||
// 如果在节流期间有新的变化,则重置尾触发
|
||||
clearTimeout(trailingTimeout);
|
||||
trailingTimeout = setTimeout(() => {
|
||||
handleWindowResize();
|
||||
layerManager?.updateLayersObjectsInteractivity?.();
|
||||
setTimeout(() => {
|
||||
layerManager?.updateLayersObjectsInteractivity?.();
|
||||
});
|
||||
lastRunTime = Date.now();
|
||||
}, throttleDelay);
|
||||
}
|
||||
clearTimeout(trailingTimeout);
|
||||
trailingTimeout = setTimeout(() => {
|
||||
optimizeCanvasRendering(canvasManager.canvas, ()=> handleWindowResize());
|
||||
}, throttleDelay);
|
||||
});
|
||||
observer.observe(canvasContainerRef.value);
|
||||
// 使用window的resize事件代替ResizeObserver
|
||||
@@ -527,12 +542,11 @@ watchEffect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// if (import.meta.hot) {
|
||||
// // 热更新 ?
|
||||
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
||||
// return; // 开发环境下不卸载组件
|
||||
// }
|
||||
onBeforeUnmount(async () => {
|
||||
observer.unobserve(canvasContainerRef.value);
|
||||
// const extraInfo = await canvasManager.exportExtraInfo();
|
||||
// emit("before-unmount-export-extra-info", extraInfo);
|
||||
|
||||
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
||||
canvasManager?.dispose?.();
|
||||
commandManager?.dispose?.();
|
||||
@@ -541,6 +555,7 @@ onBeforeUnmount(() => {
|
||||
toolManager?.dispose?.();
|
||||
liquifyManager?.dispose?.();
|
||||
selectionManager?.dispose?.();
|
||||
partManager?.dispose?.();
|
||||
redGreenModeManager?.dispose?.();
|
||||
// minimapManager?.dispose?.();
|
||||
canvasManager = null;
|
||||
@@ -550,25 +565,25 @@ onBeforeUnmount(() => {
|
||||
toolManager = null;
|
||||
liquifyManager = null;
|
||||
selectionManager = null;
|
||||
partManager = null;
|
||||
redGreenModeManager = null;
|
||||
// fabric.Object.prototype.controls.deleteControl = undefined;
|
||||
|
||||
// 移除window resize事件监听
|
||||
// window.removeEventListener("resize", handleWindowResize);
|
||||
observer.unobserve(canvasContainerRef.value);
|
||||
});
|
||||
|
||||
// 窗口大小变化处理函数
|
||||
function handleWindowResize() {
|
||||
console.log(132);
|
||||
async function handleWindowResize() {
|
||||
console.log("==========画布窗口大小变化==========");
|
||||
// 使用requestAnimationFrame来防止频繁更新
|
||||
setTimeout(() => {
|
||||
// 更新画布大小并自动居中所有元素
|
||||
updateCanvasSize();
|
||||
|
||||
// 确保显示的缩放信息是最新的
|
||||
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
|
||||
});
|
||||
await new Promise(requestAnimationFrame);
|
||||
if(!canvasManager) return;
|
||||
updateCanvasSize();
|
||||
// 确保显示的缩放信息是最新的
|
||||
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
|
||||
await new Promise(requestAnimationFrame);
|
||||
await layerManager?.updateLayersObjectsInteractivity?.();
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
@@ -715,47 +730,17 @@ function addRemoveBtn(fun) {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFun() {
|
||||
removeLayer(layerManager.activeLayerId.value);
|
||||
function deleteFun(e, control) {
|
||||
const target = control.target;
|
||||
if(target.onDelete){
|
||||
target.onDelete(target);
|
||||
}else if(target.id){
|
||||
removeLayer(layerManager.activeLayerId.value);
|
||||
}
|
||||
}
|
||||
|
||||
function removeLayer(layerId) {
|
||||
// Check if this is the last layer - prevent deletion
|
||||
var isChild = false;
|
||||
var parentLength = 0;
|
||||
layers.value.forEach((layer) => {
|
||||
if(layer.children.some(v => v.id == layerId)){
|
||||
isChild = true;
|
||||
parentLength = layer.children.length;
|
||||
}
|
||||
})
|
||||
if(isChild && parentLength == 1 || layers.value.length <= 3){
|
||||
console.warn(
|
||||
"Cannot delete the last layer. At least one layer must remain."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
layerManager.removeLayer(layerId);
|
||||
// 此处删除画布上内容导致撤回操作无效(多余)
|
||||
// if (canvasManager && canvasManager.canvas) {
|
||||
// const layerToRemove = layers.value.find((l) => l.id === layerId);
|
||||
// if (layerToRemove) {
|
||||
// const elementIds = layerToRemove?.fabricObjects?.map((e) => e.id);
|
||||
// elementIds.forEach((elementId) => {
|
||||
// const objectToRemove = canvasManager.canvas
|
||||
// .getObjects()
|
||||
// .find((obj) => obj.id === elementId);
|
||||
// if (objectToRemove) {
|
||||
// canvasManager.canvas.remove(objectToRemove);
|
||||
// }
|
||||
// });
|
||||
// if (activeLayerId.value === layerId) {
|
||||
// activeElementId.value = null;
|
||||
// }
|
||||
// canvasManager.canvas.renderAll();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
function triggerImageUpload() {
|
||||
@@ -902,13 +887,18 @@ const changeCanvas = async (command) => {
|
||||
...command, // 传递完整的命令数据
|
||||
};
|
||||
emit("changeCanvas", commandData);
|
||||
if (command.canUndo || command.canRedo) {
|
||||
canvasManager.changeCanvas(commandData);
|
||||
if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) {
|
||||
setTimeout(async () => {
|
||||
const imageData = await canvasManager.exportImage({
|
||||
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
||||
isCropByBg: true,
|
||||
});
|
||||
emit("trigger-red-green-mouseup", imageData);
|
||||
try {
|
||||
const imageData = await canvasManager.exportImage({
|
||||
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
||||
isCropByBg: true,
|
||||
});
|
||||
emit("trigger-red-green-mouseup", imageData);
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
@@ -918,6 +908,14 @@ const cropImage = (url) => {
|
||||
return cropImageRef.value.open(url)
|
||||
};
|
||||
provide("cropImage", cropImage); // 提供给子组件使用
|
||||
// 颜色选择器组件
|
||||
const palletPanelRef = ref(null);
|
||||
const palletPanel = (url) => {
|
||||
return palletPanelRef.value.open(url)
|
||||
};
|
||||
provide("palletPanel", palletPanel); // 提供给子组件使用
|
||||
|
||||
|
||||
|
||||
// 处理画布容器的拖放事件
|
||||
const isDragOver = ref(false);
|
||||
@@ -982,6 +980,18 @@ defineExpose({
|
||||
...opts,
|
||||
});
|
||||
},
|
||||
updateOtherLayers: async (otherData) => {
|
||||
await new Promise((resolve) => optimizeCanvasRendering(canvasManager.canvas, resolve));
|
||||
await canvasManager?.createOtherLayers?.(otherData, true);
|
||||
layerManager.activeLayerId.value = ""
|
||||
layerManager?.sortLayers();
|
||||
await layerManager?.updateLayersObjectsInteractivity?.(true);
|
||||
canvasManager?.canvas?.renderAll();
|
||||
setTimeout(() => {
|
||||
canvasManager?.updateAllThumbnails();
|
||||
}, 500);
|
||||
return true;
|
||||
},
|
||||
//图片url或者base64
|
||||
addImageToLayer: async (
|
||||
url,
|
||||
@@ -1014,6 +1024,10 @@ defineExpose({
|
||||
exportImage: ({
|
||||
isContainBg = false, // 是否包含背景图层
|
||||
isContainFixed = false, // 是否包含固定图层
|
||||
isContainFixedOther = true, // 是否包含其他固定图层--颜色图层
|
||||
isPrintTrimsNoRepeat = true, // 是否包含印花图层的不平铺
|
||||
isPrintTrimsRepeat = true, // 是否包含印花图层的平铺
|
||||
isContainNormalLayer = true, // 是否包含普通图层
|
||||
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小
|
||||
layerId = "", // 导出具体图层ID
|
||||
layerIdArray = [], // 导出多个图层ID数组
|
||||
@@ -1023,6 +1037,10 @@ defineExpose({
|
||||
return canvasManager.exportImage({
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther,
|
||||
isPrintTrimsNoRepeat,
|
||||
isPrintTrimsRepeat,
|
||||
isContainNormalLayer,
|
||||
isCropByBg,
|
||||
layerId,
|
||||
layerIdArray,
|
||||
@@ -1030,6 +1048,10 @@ defineExpose({
|
||||
isEnhanceImg,
|
||||
});
|
||||
},
|
||||
// 导出颜色图层
|
||||
exportColorLayer: () => {
|
||||
return canvasManager.exportColorLayer();
|
||||
},
|
||||
/**
|
||||
* 移动图层位置
|
||||
* @param {string} layerId 图层ID
|
||||
@@ -1048,6 +1070,14 @@ defineExpose({
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 导出所有信息
|
||||
* @returns {Object} 包含所有图层信息的对象
|
||||
*/
|
||||
exportExtraInfo: () => {
|
||||
return canvasManager.exportExtraInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
@@ -1179,6 +1209,7 @@ defineExpose({
|
||||
v-if="canvasManagerLoaded"
|
||||
:activeTool="activeTool"
|
||||
:isRedGreenMode="isRedGreenMode"
|
||||
:clothingMinIOPath="props.clothingMinIOPath"
|
||||
@tool-selected="handleToolSelect"
|
||||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||||
@toggle-red-green-mode="toggleRedGreenMode"
|
||||
@@ -1245,6 +1276,20 @@ defineExpose({
|
||||
:commandManager="commandManager"
|
||||
:selectionManager="selectionManager"
|
||||
:layerManager="layerManager"
|
||||
:canvasManager="canvasManager"
|
||||
:toolManager="toolManager"
|
||||
:activeTool="activeTool"
|
||||
/>
|
||||
|
||||
<!-- 部件选取面板 -->
|
||||
<PartSelectorPanel
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager && canvasManager.canvas"
|
||||
:commandManager="commandManager"
|
||||
:selectionManager="selectionManager"
|
||||
:partManager="partManager"
|
||||
:layerManager="layerManager"
|
||||
:canvasManager="canvasManager"
|
||||
:toolManager="toolManager"
|
||||
:activeTool="activeTool"
|
||||
/>
|
||||
@@ -1269,6 +1314,7 @@ defineExpose({
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 图层面板组件 -->
|
||||
@@ -1298,9 +1344,11 @@ defineExpose({
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- 裁剪图片组件 -->
|
||||
<CropImage ref="cropImageRef" />
|
||||
</div>
|
||||
<!-- 裁剪图片组件 -->
|
||||
<CropImage ref="cropImageRef" />
|
||||
<!-- 颜色选择器组件 -->
|
||||
<PalletPanel ref="palletPanelRef" />
|
||||
|
||||
<!-- <div class="footer-actions">
|
||||
<button class="share-btn">Share</button>
|
||||
@@ -1399,6 +1447,7 @@ defineExpose({
|
||||
/* background-color: #f8f8f8; */
|
||||
:deep(.canvas-container) {
|
||||
position: absolute !important;
|
||||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1411,33 +1460,31 @@ defineExpose({
|
||||
}
|
||||
|
||||
.background-grid {
|
||||
--offsetX: 0px;
|
||||
--offsetY: 0px;
|
||||
--size: 8px;
|
||||
--color: #dedcdc;
|
||||
--offsetX: 50%;
|
||||
--offsetY: 50%;
|
||||
--size: 10px;
|
||||
--color: rgba(229, 229,229,0.5);
|
||||
background-image: -webkit-linear-gradient(
|
||||
45deg,
|
||||
var(--color) 25%,
|
||||
90deg,
|
||||
var(--color) 1px,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
var(--color) 0
|
||||
),
|
||||
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent
|
||||
75%, var(--color) 0);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--color) 25%,
|
||||
-webkit-linear-gradient(
|
||||
0,
|
||||
var(--color) 1px,
|
||||
transparent 0,
|
||||
);
|
||||
background-image:linear-gradient(
|
||||
90deg,
|
||||
var(--color) 1px,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
var(--color) 0
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
var(--color) 25%,
|
||||
0,
|
||||
var(--color) 1px,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
var(--color) 0
|
||||
);
|
||||
background-color: #fafafa;
|
||||
background-position: var(--offsetX) var(--offsetY),
|
||||
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
|
||||
background-size: calc(var(--size) * 2) calc(var(--size) * 2);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { findObjectById } from "../utils/helper";
|
||||
import { createRasterizedImage } from "../utils/selectionToImage";
|
||||
import { OperationType, SpecialLayerId } from "../utils/layerHelper";
|
||||
|
||||
/**
|
||||
* 图片导出管理器
|
||||
@@ -18,26 +19,35 @@ export class ExportManager {
|
||||
* @param {Object} options 导出选项
|
||||
* @param {Boolean} options.isContainBg 是否包含背景图层
|
||||
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} options.isContainNormalLayer 是否包含普通图层
|
||||
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||
* @param {String} options.layerId 导出具体图层ID
|
||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||
* @param {Array} options.layerIdArray2 导出多个图层ID数组2
|
||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||
* @param {Array} options.excludedLayers 排除的图层ID数组
|
||||
* @returns {String} 导出的图片数据URL
|
||||
*/
|
||||
exportImage(options = {}) {
|
||||
async exportImage(options = {}) {
|
||||
const {
|
||||
isContainBg = false,
|
||||
isContainFixed = false,
|
||||
isContainFixedOther = false, // 是否包含其他固定图层
|
||||
isContainNormalLayer = true, // 是否包含普通图层
|
||||
isCropByBg = false, // 是否使用背景大小裁剪
|
||||
layerId = "",
|
||||
layerIdArray = [],
|
||||
layerIdArray2 = null,
|
||||
expPicType = "png",
|
||||
restoreOpacityInRedGreen = true,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
excludedLayers = [], // 排除的图层ID数组
|
||||
} = options;
|
||||
try {
|
||||
|
||||
// 检查是否为红绿图模式
|
||||
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
||||
// 如果指定了具体图层ID,导出指定图层
|
||||
@@ -48,7 +58,7 @@ export class ExportManager {
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,10 +69,12 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer, // 是否包含普通图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,10 +83,14 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer, // 是否包含普通图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
layerIdArray2,
|
||||
excludedLayers, // 排除的图层ID数组
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("导出图片失败:", error);
|
||||
@@ -128,8 +144,6 @@ export class ExportManager {
|
||||
objectsToExport,
|
||||
expPicType,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,6 +163,8 @@ export class ExportManager {
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isContainNormalLayer 是否包含普通图层
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||
@@ -161,6 +177,8 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer = true, // 是否包含普通图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
@@ -174,7 +192,9 @@ export class ExportManager {
|
||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||
layerIdArray,
|
||||
isContainBg,
|
||||
isContainFixed
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer, // 是否包含普通图层
|
||||
);
|
||||
|
||||
if (objectsToExport.length === 0) {
|
||||
@@ -206,10 +226,13 @@ export class ExportManager {
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isContainNormalLayer 是否包含普通图层
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||
* @param {Boolean} isEnhanceImg 是否是增强图片
|
||||
* @param {Array} layerIdArray 导出多个图层ID数组2
|
||||
* @returns {String} 图片数据URL
|
||||
* @private
|
||||
*/
|
||||
@@ -217,16 +240,23 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer, // 是否包含普通图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
layerIdArray, // 导出所有图层
|
||||
excludedLayers, // 排除的图层ID数组
|
||||
) {
|
||||
// 按图层顺序收集对象(从底到顶)
|
||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||
null, // 导出所有图层
|
||||
layerIdArray, // 导出所有图层
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isContainNormalLayer, // 是否包含普通图层
|
||||
excludedLayers,
|
||||
);
|
||||
|
||||
if (objectsToExport.length === 0) {
|
||||
@@ -282,10 +312,11 @@ export class ExportManager {
|
||||
/**
|
||||
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象)
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Boolean} isChildren 是否递归收集子图层的对象
|
||||
* @returns {Array} 画布中的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsFromLayer(layer) {
|
||||
_collectObjectsFromLayer(layer, isChildren = true) {
|
||||
if (!layer) {
|
||||
return [];
|
||||
}
|
||||
@@ -314,10 +345,10 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
// 递归收集子图层的对象
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
if (isChildren && layer.children && layer.children.length > 0) {
|
||||
for (let i = layer.children.length - 1; i >= 0; i--) {
|
||||
const childLayer = layer.children[i];
|
||||
const childObjects = this._collectObjectsFromLayer(childLayer);
|
||||
const childObjects = this._collectObjectsFromLayer(childLayer, isChildren);
|
||||
realObjects.push(...childObjects);
|
||||
}
|
||||
}
|
||||
@@ -383,12 +414,15 @@ export class ExportManager {
|
||||
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isContainNormalLayer 是否包含普通图层
|
||||
* @param {Array} excludedLayers 排除的图层ID数组
|
||||
* @returns {Array} 按正确顺序排列的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer, excludedLayers) {
|
||||
const objectsToExport = [];
|
||||
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
|
||||
const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表
|
||||
|
||||
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
||||
for (let i = allLayers.length - 1; i >= 0; i--) {
|
||||
@@ -398,11 +432,11 @@ export class ExportManager {
|
||||
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
|
||||
|
||||
// 检查图层类型过滤条件
|
||||
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
|
||||
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer))
|
||||
continue;
|
||||
|
||||
if (layer.visible) {
|
||||
const layerObjects = this._collectObjectsFromLayer(layer);
|
||||
const layerObjects = this._collectObjectsFromLayer(layer, false);
|
||||
objectsToExport.push(...layerObjects);
|
||||
}
|
||||
}
|
||||
@@ -411,15 +445,19 @@ export class ExportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扁平化的图层列表(包含子图层)
|
||||
* 获取扁平化的图层列表(包含子图层),排除指定的图层
|
||||
* @param {Array} excludedLayers 排除的图层ID数组
|
||||
* @returns {Array} 扁平化的图层数组
|
||||
* @private
|
||||
*/
|
||||
_getAllLayersFlattened() {
|
||||
_getAllLayersFlattened(excludedLayers) {
|
||||
const flattenedLayers = [];
|
||||
const rootLayers = this._getAllLayers();
|
||||
|
||||
const flattenLayer = (layer) => {
|
||||
// 检查是否在排除列表中
|
||||
if (excludedLayers && excludedLayers.includes(layer.id)) return;
|
||||
|
||||
flattenedLayers.push(layer);
|
||||
|
||||
// 递归处理子图层
|
||||
@@ -434,7 +472,6 @@ export class ExportManager {
|
||||
for (const layer of rootLayers) {
|
||||
flattenLayer(layer);
|
||||
}
|
||||
|
||||
return flattenedLayers;
|
||||
}
|
||||
|
||||
@@ -555,37 +592,22 @@ export class ExportManager {
|
||||
);
|
||||
}
|
||||
|
||||
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
||||
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
|
||||
|
||||
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
||||
const canvasWidth = Math.round(fixedBounds.width);
|
||||
const canvasHeight = Math.round(fixedBounds.height);
|
||||
const canvasWidth = (fixedLayerObject.width);
|
||||
const canvasHeight = (fixedLayerObject.height);
|
||||
|
||||
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
||||
console.log("固定图层边界:", fixedBounds);
|
||||
|
||||
// 创建固定尺寸的临时画布
|
||||
const scaleFactor = 2; // 高清导出
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = canvasWidth * scaleFactor;
|
||||
tempCanvas.height = canvasHeight * scaleFactor;
|
||||
tempCanvas.style.width = canvasWidth + "px";
|
||||
tempCanvas.style.height = canvasHeight + "px";
|
||||
|
||||
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
|
||||
const tempFabricCanvas = new fabric.StaticCanvas()
|
||||
tempFabricCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
backgroundColor: null,
|
||||
enableRetinaScaling: true,
|
||||
// enableRetinaScaling: true,
|
||||
imageSmoothingEnabled: true,
|
||||
});
|
||||
tempFabricCanvas.setZoom(1);
|
||||
|
||||
// tempFabricCanvas.setZoom(1);
|
||||
console.log("==========", fixedLayerObject)
|
||||
try {
|
||||
// 获取裁剪路径对象(如果存在)
|
||||
const clipPathObject = await this._getClipPathObject(fixedBounds);
|
||||
|
||||
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
||||
for (let i = 0; i < objectsToExport.length; i++) {
|
||||
const obj = objectsToExport[i];
|
||||
@@ -594,20 +616,17 @@ export class ExportManager {
|
||||
restoreOpacityInRedGreen && true
|
||||
);
|
||||
if (cloned) {
|
||||
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
|
||||
cloned.set({
|
||||
left: cloned.left - fixedBounds.left,
|
||||
top: cloned.top - fixedBounds.top,
|
||||
left: canvasWidth / 2,
|
||||
top: canvasHeight / 2,
|
||||
scaleX: cloned.scaleX / fixedLayerObject.scaleX,
|
||||
scaleY: cloned.scaleY / fixedLayerObject.scaleY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
console.log("==========", {...cloned})
|
||||
// 更新对象坐标
|
||||
cloned.setCoords();
|
||||
|
||||
// 设置裁剪路径到对象
|
||||
if (clipPathObject) {
|
||||
cloned.clipPath = clipPathObject;
|
||||
}
|
||||
|
||||
tempFabricCanvas.add(cloned);
|
||||
}
|
||||
}
|
||||
@@ -616,7 +635,7 @@ export class ExportManager {
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 生成图片
|
||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
||||
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
|
||||
} finally {
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
}
|
||||
@@ -736,7 +755,7 @@ export class ExportManager {
|
||||
*/
|
||||
_cloneObjectAsync(
|
||||
obj,
|
||||
propertiesToInclude = ["id", "layerId", "layerName", "name"]
|
||||
propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"]
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!obj) {
|
||||
@@ -1031,10 +1050,12 @@ export class ExportManager {
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isContainNormalLayer 是否包含普通图层
|
||||
* @returns {Boolean} 是否应该包含
|
||||
* @private
|
||||
*/
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther, isContainNormalLayer) {
|
||||
if (!layer) return false;
|
||||
|
||||
// 检查背景图层
|
||||
@@ -1047,7 +1068,17 @@ export class ExportManager {
|
||||
return isContainFixed;
|
||||
}
|
||||
|
||||
// 普通图层总是包含
|
||||
return true;
|
||||
// 检查其他固定图层
|
||||
if (layer.isFixedOther) {
|
||||
return isContainFixedOther;
|
||||
}
|
||||
|
||||
// 印花图层始终导出
|
||||
if (layer.isPrintTrims || layer.isPrintTrimsGroup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 普通图层
|
||||
return isContainNormalLayer;
|
||||
}
|
||||
}
|
||||
|
||||