Compare commits
162 Commits
main
...
7bf1a0bd57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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": "裁剪",
|
||||
|
||||
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/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({
|
||||
// 设置绝对定位
|
||||
|
||||
335
src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
Normal file
@@ -0,0 +1,335 @@
|
||||
import { Command } from "./Command";
|
||||
import { findLayerRecursively } from "../utils/layerHelper";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
findObjectById,
|
||||
generateId,
|
||||
insertObjectAtZIndex,
|
||||
removeCanvasObjectByObject,
|
||||
createPatternTransform,
|
||||
} 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;
|
||||
// 创建透明 Canvas
|
||||
const tcanvas = document.createElement('canvas');
|
||||
tcanvas.width = image.width + object.fill_.gapX;
|
||||
tcanvas.height = image.height + object.fill_.gapY;
|
||||
const ctx = tcanvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
const fill = object.get("fill");
|
||||
fill.source = tcanvas;
|
||||
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,440 @@
|
||||
<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>
|
||||
|
||||
<div class="tool-types">
|
||||
<div
|
||||
v-for="item in toolList"
|
||||
:key="item.type"
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: selectionType === item.type },
|
||||
]"
|
||||
@click="setSelectionType(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="copySelectionToNewLayer">
|
||||
<svg-icon name="CPaste" size="16" />
|
||||
<span class="btn-text">{{
|
||||
$t("Canvas.creation")
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="cutSelectionToNewLayer">
|
||||
<svg-icon name="CCut" size="26" />
|
||||
<span class="btn-text">{{
|
||||
$t("Canvas.CreateAndCopy")
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="clearSelectionContent">
|
||||
<svg-icon name="CClear" size="18" />
|
||||
<span class="btn-text">{{
|
||||
$t("Canvas.TheClearlySelectedContent")
|
||||
}}</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,
|
||||
},
|
||||
selectionManager: {
|
||||
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 selectionType = ref("rectangle");
|
||||
//打开隐藏操作面板
|
||||
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: "CRectangle",
|
||||
size: "26",
|
||||
},
|
||||
{
|
||||
type: OperationType.PART_BRUSH,
|
||||
label: "Brush Selection",
|
||||
icon: "CBrush",
|
||||
size: "24",
|
||||
},
|
||||
{
|
||||
type: OperationType.PART_ERASER,
|
||||
label: "Erase",
|
||||
icon: "CEraser",
|
||||
size: "24",
|
||||
},
|
||||
];
|
||||
|
||||
// 国际化
|
||||
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();
|
||||
// 根据工具类型设置选区类型
|
||||
selectionType.value = newTool;
|
||||
|
||||
// 更新选区管理器的选区类型
|
||||
if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(selectionType.value);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
visible.value = true;
|
||||
closePanel.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区类型
|
||||
*/
|
||||
function setSelectionType(type) {
|
||||
selectionType.value = type;
|
||||
|
||||
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager
|
||||
if (props.toolManager) {
|
||||
props.toolManager.setToolWithCommand(type);
|
||||
}
|
||||
|
||||
// 备用方案:如果没有 toolManager,直接更新 selectionManager
|
||||
else if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(type);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.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(3, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行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>
|
||||
@@ -166,6 +166,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"),
|
||||
@@ -412,8 +425,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, // 默认配置
|
||||
@@ -78,7 +88,11 @@ const props = defineProps({
|
||||
},
|
||||
clothingImageUrl: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL
|
||||
default: "", // 衣服底图URL-线稿
|
||||
},
|
||||
clothingImageUrl2: {
|
||||
type: String,
|
||||
default: "", // 衣服底图URL-上色
|
||||
},
|
||||
redGreenImageUrl: {
|
||||
type: String,
|
||||
@@ -172,6 +186,7 @@ let keyboardManager = null;
|
||||
let toolManager = null;
|
||||
let liquifyManager = null;
|
||||
let selectionManager = null;
|
||||
let partManager = null;
|
||||
let redGreenModeManager = null;
|
||||
|
||||
// 快捷键帮助模态框状态
|
||||
@@ -213,6 +228,7 @@ function handleCanvasInit(isLoadJson = false) {
|
||||
keyboardManager,
|
||||
liquifyManager,
|
||||
selectionManager,
|
||||
partManager,
|
||||
redGreenModeManager,
|
||||
});
|
||||
}
|
||||
@@ -250,6 +266,8 @@ onMounted(async () => {
|
||||
canvasColor,
|
||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||
isFixedErasable: props.isFixedErasable,
|
||||
props,
|
||||
emit,
|
||||
});
|
||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||
canvasManager.activeLayerId = activeLayerId;
|
||||
@@ -307,6 +325,7 @@ onMounted(async () => {
|
||||
canvas: canvasManager.canvas,
|
||||
commandManager,
|
||||
layerManager,
|
||||
canvasManager,
|
||||
toolManager,
|
||||
isRedGreenMode,
|
||||
pasteText: (text) => {
|
||||
@@ -362,6 +381,13 @@ onMounted(async () => {
|
||||
});
|
||||
canvasManager.setSelectionManager(selectionManager);
|
||||
|
||||
// 初始化部件选择管理器
|
||||
partManager = new PartManager({
|
||||
canvas: canvasManager.canvas,
|
||||
layerManager,
|
||||
});
|
||||
canvasManager.setPartManager(partManager);
|
||||
|
||||
if (props.canvasJSON) {
|
||||
// 如果传入了初始JSON数据,加载到画布上
|
||||
if (typeof props.canvasJSON === "string") {
|
||||
@@ -435,6 +461,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 +496,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 +534,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 +547,7 @@ onBeforeUnmount(() => {
|
||||
toolManager?.dispose?.();
|
||||
liquifyManager?.dispose?.();
|
||||
selectionManager?.dispose?.();
|
||||
partManager?.dispose?.();
|
||||
redGreenModeManager?.dispose?.();
|
||||
// minimapManager?.dispose?.();
|
||||
canvasManager = null;
|
||||
@@ -550,25 +557,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() {
|
||||
@@ -720,42 +727,7 @@ function deleteFun() {
|
||||
}
|
||||
|
||||
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 +874,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 +895,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 +967,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 +1011,9 @@ defineExpose({
|
||||
exportImage: ({
|
||||
isContainBg = false, // 是否包含背景图层
|
||||
isContainFixed = false, // 是否包含固定图层
|
||||
isContainFixedOther = false, // 是否包含其他固定图层
|
||||
isPrintTrimsNoRepeat = true, // 是否包含印花图层的不平铺
|
||||
isPrintTrimsRepeat = true, // 是否包含印花图层的平铺
|
||||
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小
|
||||
layerId = "", // 导出具体图层ID
|
||||
layerIdArray = [], // 导出多个图层ID数组
|
||||
@@ -1023,6 +1023,9 @@ defineExpose({
|
||||
return canvasManager.exportImage({
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther,
|
||||
isPrintTrimsNoRepeat,
|
||||
isPrintTrimsRepeat,
|
||||
isCropByBg,
|
||||
layerId,
|
||||
layerIdArray,
|
||||
@@ -1030,6 +1033,10 @@ defineExpose({
|
||||
isEnhanceImg,
|
||||
});
|
||||
},
|
||||
// 导出颜色图层
|
||||
exportColorLayer: () => {
|
||||
return canvasManager.exportColorLayer();
|
||||
},
|
||||
/**
|
||||
* 移动图层位置
|
||||
* @param {string} layerId 图层ID
|
||||
@@ -1048,6 +1055,14 @@ defineExpose({
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 导出所有信息
|
||||
* @returns {Object} 包含所有图层信息的对象
|
||||
*/
|
||||
exportExtraInfo: () => {
|
||||
return canvasManager.exportExtraInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* 拖拽排序图层
|
||||
* @param {number} oldIndex 原索引
|
||||
@@ -1245,6 +1260,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 +1298,7 @@ defineExpose({
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 图层面板组件 -->
|
||||
@@ -1298,9 +1328,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 +1431,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 +1444,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,33 @@ 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.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, // 是否包含其他固定图层
|
||||
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 +56,7 @@ export class ExportManager {
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,10 +67,11 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,10 +80,13 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg,
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
layerIdArray2,
|
||||
excludedLayers, // 排除的图层ID数组
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("导出图片失败:", error);
|
||||
@@ -128,8 +140,6 @@ export class ExportManager {
|
||||
objectsToExport,
|
||||
expPicType,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,6 +159,7 @@ export class ExportManager {
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||
@@ -161,6 +172,7 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
@@ -174,7 +186,8 @@ export class ExportManager {
|
||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||
layerIdArray,
|
||||
isContainBg,
|
||||
isContainFixed
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
);
|
||||
|
||||
if (objectsToExport.length === 0) {
|
||||
@@ -206,10 +219,12 @@ export class ExportManager {
|
||||
* @param {String} expPicType 导出类型
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||
* @param {Boolean} isEnhanceImg 是否是增强图片
|
||||
* @param {Array} layerIdArray 导出多个图层ID数组2
|
||||
* @returns {String} 图片数据URL
|
||||
* @private
|
||||
*/
|
||||
@@ -217,16 +232,21 @@ export class ExportManager {
|
||||
expPicType,
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
isRedGreenMode,
|
||||
restoreOpacityInRedGreen,
|
||||
isCropByBg, // 是否使用背景大小裁剪
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
isEnhanceImg, // 是否是增强图片
|
||||
layerIdArray, // 导出所有图层
|
||||
excludedLayers, // 排除的图层ID数组
|
||||
) {
|
||||
// 按图层顺序收集对象(从底到顶)
|
||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||
null, // 导出所有图层
|
||||
layerIdArray, // 导出所有图层
|
||||
isContainBg,
|
||||
isContainFixed,
|
||||
isContainFixedOther, // 是否包含其他固定图层
|
||||
excludedLayers,
|
||||
);
|
||||
|
||||
if (objectsToExport.length === 0) {
|
||||
@@ -282,10 +302,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 +335,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 +404,14 @@ export class ExportManager {
|
||||
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @param {Array} excludedLayers 排除的图层ID数组
|
||||
* @returns {Array} 按正确顺序排列的真实对象数组
|
||||
* @private
|
||||
*/
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, excludedLayers) {
|
||||
const objectsToExport = [];
|
||||
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
|
||||
const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表
|
||||
|
||||
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
||||
for (let i = allLayers.length - 1; i >= 0; i--) {
|
||||
@@ -398,11 +421,11 @@ export class ExportManager {
|
||||
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
|
||||
|
||||
// 检查图层类型过滤条件
|
||||
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
|
||||
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther))
|
||||
continue;
|
||||
|
||||
if (layer.visible) {
|
||||
const layerObjects = this._collectObjectsFromLayer(layer);
|
||||
const layerObjects = this._collectObjectsFromLayer(layer, false);
|
||||
objectsToExport.push(...layerObjects);
|
||||
}
|
||||
}
|
||||
@@ -411,15 +434,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 +461,6 @@ export class ExportManager {
|
||||
for (const layer of rootLayers) {
|
||||
flattenLayer(layer);
|
||||
}
|
||||
|
||||
return flattenedLayers;
|
||||
}
|
||||
|
||||
@@ -555,37 +581,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 +605,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 +624,7 @@ export class ExportManager {
|
||||
tempFabricCanvas.renderAll();
|
||||
|
||||
// 生成图片
|
||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
||||
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
|
||||
} finally {
|
||||
this._cleanupTempCanvas(tempFabricCanvas);
|
||||
}
|
||||
@@ -736,7 +744,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 +1039,11 @@ export class ExportManager {
|
||||
* @param {Object} layer 图层对象
|
||||
* @param {Boolean} isContainBg 是否包含背景图层
|
||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||
* @returns {Boolean} 是否应该包含
|
||||
* @private
|
||||
*/
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther) {
|
||||
if (!layer) return false;
|
||||
|
||||
// 检查背景图层
|
||||
@@ -1047,6 +1056,11 @@ export class ExportManager {
|
||||
return isContainFixed;
|
||||
}
|
||||
|
||||
// 检查其他固定图层
|
||||
if (layer.isFixedOther) {
|
||||
return isContainFixedOther;
|
||||
}
|
||||
|
||||
// 普通图层总是包含
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "../commands/ObjectLayerCommands";
|
||||
import {
|
||||
LayerType,
|
||||
SpecialLayerId,
|
||||
BlendMode,
|
||||
createLayer,
|
||||
createBackgroundLayer,
|
||||
@@ -198,9 +199,12 @@ export class LayerManager {
|
||||
if (!this.canvas) return;
|
||||
if (isUseOptimize) {
|
||||
// 优化渲染 - 统一批处理 支持异步回调
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 应用图层交互规则
|
||||
await this._applyInteractionRules({ isMoveing });
|
||||
await new Promise((resolve) => {
|
||||
optimizeCanvasRendering(this.canvas, async () => {
|
||||
// 应用图层交互规则
|
||||
await this._applyInteractionRules({ isMoveing });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 直接应用图层交互规则
|
||||
@@ -332,7 +336,6 @@ export class LayerManager {
|
||||
const objects = this.canvas.getObjects();
|
||||
const editorMode = this.editorMode || CanvasConfig.defaultTool;
|
||||
const layers = this.layers?.value || [];
|
||||
|
||||
// 创建缓存以避免重复查找
|
||||
const layerMap = {};
|
||||
layers.forEach((layer) => {
|
||||
@@ -343,35 +346,36 @@ export class LayerManager {
|
||||
});
|
||||
|
||||
// 批量更新对象
|
||||
objects.forEach(async (obj) => {
|
||||
const layer = layerMap[obj.layerId];
|
||||
for(let obj of objects){
|
||||
let layer = layerMap[obj.layerId];
|
||||
|
||||
if (!obj.layerId) {
|
||||
// 没有关联图层的对象使用默认设置
|
||||
obj.selectable = false;
|
||||
obj.evented = false;
|
||||
obj.erasable = false; // 未关联图层的对象不可擦除
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!layer) return;
|
||||
if (!layer) break;
|
||||
|
||||
// 设置一级图层对象的交互性
|
||||
await this._setObjectInteractivity(obj, layer, editorMode);
|
||||
|
||||
// 设置子图层对象的交互性
|
||||
layer?.children?.forEach(async (childLayer) => {
|
||||
const childObj = this.canvas
|
||||
for(let childLayer of layer.children){
|
||||
let childObj = this.canvas
|
||||
.getObjects()
|
||||
.find((o) => o.layerId === childLayer.id);
|
||||
if (childObj) {
|
||||
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
// 设置裁剪对象
|
||||
layers.forEach(async (layer) => {
|
||||
for(let layer of layers){
|
||||
if(layer.id === SpecialLayerId.COLOR) break;
|
||||
let clippingMaskFabricObject = null;
|
||||
if (layer.clippingMask) {
|
||||
// 反序列化 clippingMask
|
||||
@@ -379,7 +383,7 @@ export class LayerManager {
|
||||
layer.clippingMask,
|
||||
this.canvas
|
||||
);
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
|
||||
clippingMaskFabricObject.set({
|
||||
// 设置绝对定位
|
||||
@@ -403,7 +407,7 @@ export class LayerManager {
|
||||
.find((o) => o.layerId === childLayer.id);
|
||||
if (childObj) {
|
||||
childObj.clipPath = clippingMaskFabricObject;
|
||||
childObj.dirty = true; // 标记为脏对象
|
||||
// childObj.dirty = true; // 标记为脏对象
|
||||
childObj.setCoords();
|
||||
}
|
||||
|
||||
@@ -499,7 +503,7 @@ export class LayerManager {
|
||||
isOldSelectObject
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,15 +526,16 @@ export class LayerManager {
|
||||
* @param {string} name 图层名称
|
||||
* @param {string} type 图层类型
|
||||
* @param {Object} options 额外选项
|
||||
* @param {boolean} isCmd 是否创建命令
|
||||
* @returns {string} 新创建的图层ID
|
||||
*/
|
||||
async createLayer(name = null, type = LayerType.EMPTY, options = {}) {
|
||||
async createLayer(name = null, type = LayerType.EMPTY, options = {}, isCmd = true) {
|
||||
// 生成唯一ID
|
||||
const layerId = options.id || options.layerId || generateId("layer_");
|
||||
|
||||
// 计算普通图层数量(非背景、非固定)
|
||||
const normalLayersCount = this.layers.value.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed
|
||||
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther
|
||||
).length;
|
||||
// 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置
|
||||
// 添加到图层列表
|
||||
@@ -542,7 +547,7 @@ export class LayerManager {
|
||||
// 创建新图层
|
||||
const newLayer = createLayer({
|
||||
id: layerId,
|
||||
name: name || `图层 ${normalLayersCount + 1}`,
|
||||
name: name || this.t("Canvas.EmptyLayer"),
|
||||
type: type,
|
||||
visible: true,
|
||||
locked: false,
|
||||
@@ -571,13 +576,13 @@ export class LayerManager {
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
if (this.commandManager) {
|
||||
if (isCmd && this.commandManager) {
|
||||
await this.commandManager.execute(command);
|
||||
} else {
|
||||
} else{
|
||||
await command.execute();
|
||||
}
|
||||
|
||||
return layerId;
|
||||
return isCmd ? layerId : command;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -952,18 +957,28 @@ export class LayerManager {
|
||||
// 查找要删除的图层
|
||||
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
|
||||
// 如果是背景层或固定层,不允许删除
|
||||
if (layer && (layer.isBackground || layer.isFixed)) {
|
||||
if (layer && (layer.isBackground || layer.isFixed || layer.isFixedOther)) {
|
||||
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
||||
message.warning(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
||||
message.warning(layer.isBackground ? this.t("Canvas.backLayerCannotDelete") : this.t("Canvas.fixedLayerCannotDelete"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查是否是唯一的普通图层
|
||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
||||
var isChild = false;
|
||||
var parentLength = 0;
|
||||
const normalLayers = this.layers.value.filter((layer) => {
|
||||
if(layer.children.some(v => v.id == layerId)){
|
||||
isChild = true;
|
||||
parentLength = layer.children.length;
|
||||
}
|
||||
return !layer.isFixed && !layer.isFixedOther && !layer.isBackground
|
||||
})
|
||||
// const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||
console.log("普通图层:", normalLayers)
|
||||
if (normalLayers.length === 1) {
|
||||
if (isChild ? parentLength <= 1 : false) {//normalLayers.length <= 1
|
||||
console.warn("不能删除唯一的普通图层");
|
||||
message.warning("不能删除唯一的普通图层");
|
||||
message.warning(this.t("Canvas.cannotDeleteOnlyLayer"));
|
||||
return false;
|
||||
}
|
||||
// // 如果图层有子图层,提示确认
|
||||
@@ -1132,7 +1147,7 @@ export class LayerManager {
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
console.log("==========", allObjects)
|
||||
// if (layer.fill) {
|
||||
// // 如果图层有填充颜色,设置所有对象的填充颜色
|
||||
// const { object } = findObjectById(this.canvas, layer.fill.id);
|
||||
@@ -1578,6 +1593,12 @@ export class LayerManager {
|
||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||
if (b.isFixed && !a.isFixed) return -1;
|
||||
|
||||
// 如果a是固定图层而b不是固定图层,a应该排在后面(固定图层在普通图层下方)
|
||||
if (a.isFixedOther && !b.isFixedOther) return 1;
|
||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||
if (b.isFixedOther && !a.isFixedOther) return -1;
|
||||
|
||||
|
||||
// 其他情况保持原有顺序
|
||||
return 0;
|
||||
});
|
||||
@@ -1848,9 +1869,9 @@ export class LayerManager {
|
||||
}
|
||||
|
||||
// 检查是否是唯一的普通图层
|
||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||
console.log("普通图层:", normalLayers)
|
||||
if (normalLayers.length === 1) {
|
||||
if (normalLayers.length <= 1) {
|
||||
console.warn("不能剪切唯一的普通图层");
|
||||
return null;
|
||||
}
|
||||
@@ -3250,7 +3271,7 @@ export class LayerManager {
|
||||
* @private
|
||||
*/
|
||||
_setupGroupMaskMovementSync(activeSelection, layer) {
|
||||
if (!activeSelection || !layer || !layer.clippingMask) {
|
||||
if (!activeSelection || !layer || !layer.clippingMask || layer.isPrintTrimsGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3314,7 +3335,6 @@ export class LayerManager {
|
||||
// 计算移动距离
|
||||
const deltaX = target.left - initialLeft;
|
||||
const deltaY = target.top - initialTop;
|
||||
|
||||
// 创建更新遮罩位置的命令
|
||||
const command = new UpdateGroupMaskPositionCommand({
|
||||
canvas: this.canvas,
|
||||
@@ -3419,4 +3439,22 @@ export class LayerManager {
|
||||
|
||||
console.log("🎨 已设置组遮罩移动同步 - 使用 object:modified 事件");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取印花和颜色图层设置了blendMode的图层ID
|
||||
* @returns {string[]} - 包含blendMode的图层ID数组
|
||||
*/
|
||||
getBlendModeLayerIds() {
|
||||
const blendModeLayerIds = [];
|
||||
this.layers.value.forEach(layer => {
|
||||
if(layer.id === SpecialLayerId.SPECIAL_GROUP){
|
||||
layer.children.forEach(child => {
|
||||
if(child.visible && child.blendMode && child.blendMode !== BlendMode.NORMAL){
|
||||
blendModeLayerIds.push(child.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return blendModeLayerIds;
|
||||
}
|
||||
}
|
||||
|
||||
941
src/component/Canvas/CanvasEditor/managers/PartManager.js
Normal file
@@ -0,0 +1,941 @@
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { generateId } from "../utils/helper";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { CreateSelectionCommand } from "../commands/SelectionCommands";
|
||||
import { ClearSelectionCommand } from "../commands/LassoCutoutCommand";
|
||||
|
||||
/**
|
||||
* 部件选择管理器
|
||||
*/
|
||||
export class PartManager {
|
||||
/**
|
||||
* 创建部件选择管理器
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.canvas fabric.js画布实例
|
||||
* @param {Object} options.commandManager 命令管理器实例
|
||||
* @param {Object} options.layerManager 图层管理实例
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.canvas = options.canvas;
|
||||
this.commandManager = options.commandManager;
|
||||
this.layerManager = options.layerManager;
|
||||
|
||||
// 选区状态
|
||||
this.isActive = false;
|
||||
this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串
|
||||
this.selectionObject = null; // 当前选区对象
|
||||
this.selectionId = "selection_" + Date.now();
|
||||
this.featherAmount = 0; // 羽化值
|
||||
|
||||
// 选区样式配置
|
||||
this.selectionStyle = {
|
||||
stroke: "#0096ff",
|
||||
strokeWidth: 1,
|
||||
strokeDashArray: [5, 5],
|
||||
fill: "rgba(0, 150, 255, 0.1)",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
excludeFromExport: true,
|
||||
hoverCursor: "default",
|
||||
moveCursor: "default",
|
||||
};
|
||||
|
||||
// 绘制状态
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
this.selectionPath = null; // 存储选区路径数据
|
||||
|
||||
// 自由选区相关状态
|
||||
this.drawingPoints = null;
|
||||
this.currentPathString = null;
|
||||
|
||||
// 不再直接绑定事件处理函数
|
||||
this._mouseDownHandler = null;
|
||||
this._mouseMoveHandler = null;
|
||||
this._mouseUpHandler = null;
|
||||
this._keyDownHandler = null;
|
||||
|
||||
// 选区相关的工具类型
|
||||
this.tools = [
|
||||
OperationType.PART,
|
||||
OperationType.PART_RECTANGLE,
|
||||
OperationType.PART_BRUSH,
|
||||
OperationType.PART_ERASER,
|
||||
];
|
||||
|
||||
// 当前工具
|
||||
this.currentTool = OperationType.SELECT;
|
||||
|
||||
// 选区状态变化回调
|
||||
this.onSelectionChanged = null;
|
||||
|
||||
// 不再自动初始化事件,改为手动控制
|
||||
// this.initEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前工具
|
||||
* @param {String} toolId 工具ID
|
||||
*/
|
||||
setCurrentTool(toolId) {
|
||||
this.currentTool = toolId;
|
||||
|
||||
// 检查是否为选区工具
|
||||
const wasActive = this.isActive;
|
||||
this.isActive = this.tools.includes(toolId);
|
||||
|
||||
// 如果从非选区工具切换到选区工具,初始化事件
|
||||
if (!wasActive && this.isActive) {
|
||||
this.initEvents();
|
||||
}
|
||||
// 如果从选区工具切换到非选区工具,清理事件和选区
|
||||
else if (wasActive && !this.isActive) {
|
||||
this.cleanupEvents();
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
// 根据工具类型设置选区类型
|
||||
if (this.isActive) {
|
||||
this.selectionType = toolId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化选区相关事件
|
||||
*/
|
||||
initEvents() {
|
||||
if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化
|
||||
|
||||
// 保存实例引用,用于事件处理函数中
|
||||
const self = this;
|
||||
|
||||
// 鼠标按下事件处理
|
||||
this._mouseDownHandler = (options) => {
|
||||
// 如果选区功能未激活,不处理事件
|
||||
if (!this.isActive) return;
|
||||
|
||||
// 如果点击的是已有对象且不是选区对象,则不处理
|
||||
if (
|
||||
options.target &&
|
||||
options.target.id !== this.selectionId &&
|
||||
options.target.selectable !== false &&
|
||||
options.target.type !== "selection"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 阻止事件冒泡,避免与 CanvasEventManager 冲突
|
||||
options.e.stopPropagation();
|
||||
|
||||
// 根据选区类型执行不同的起始操作
|
||||
switch (this.selectionType) {
|
||||
case OperationType.LASSO:
|
||||
this.startFreeSelection(options);
|
||||
break;
|
||||
case OperationType.LASSO_ELLIPSE:
|
||||
this.startEllipseSelection(options);
|
||||
break;
|
||||
case OperationType.LASSO_RECTANGLE:
|
||||
this.startRectangleSelection(options);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标移动事件处理
|
||||
this._mouseMoveHandler = (options) => {
|
||||
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
|
||||
if (!this.isActive || !this.drawingObject) return;
|
||||
|
||||
// 阻止事件冒泡
|
||||
options.e.stopPropagation();
|
||||
|
||||
// 根据选区类型执行不同的绘制操作
|
||||
switch (this.selectionType) {
|
||||
case OperationType.LASSO_RECTANGLE:
|
||||
this.drawRectangleSelection(options);
|
||||
break;
|
||||
case OperationType.LASSO_ELLIPSE:
|
||||
this.drawEllipseSelection(options);
|
||||
break;
|
||||
case OperationType.LASSO:
|
||||
this.drawFreeSelection(options);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标抬起事件处理
|
||||
this._mouseUpHandler = (options) => {
|
||||
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
|
||||
if (!this.isActive || !this.drawingObject) return;
|
||||
|
||||
// 阻止事件冒泡
|
||||
if (options && options.e) {
|
||||
options.e.stopPropagation();
|
||||
}
|
||||
|
||||
// 根据选区类型执行不同的完成操作
|
||||
switch (this.selectionType) {
|
||||
case OperationType.LASSO_RECTANGLE:
|
||||
this.endRectangleSelection();
|
||||
break;
|
||||
case OperationType.LASSO_ELLIPSE:
|
||||
this.endEllipseSelection();
|
||||
break;
|
||||
case OperationType.LASSO:
|
||||
this.endFreeSelection();
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果有命令管理器,使用命令模式记录选区创建
|
||||
if (this.commandManager && this.selectionObject) {
|
||||
this.commandManager.execute(
|
||||
new CreateSelectionCommand({
|
||||
canvas: this.canvas,
|
||||
selectionManager: this,
|
||||
selectionObject: this.selectionObject,
|
||||
selectionType: this.selectionType,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
this._keyDownHandler = (event) => {
|
||||
// 只在选区功能激活时处理键盘事件
|
||||
if (!this.isActive) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
// ESC键取消当前选区操作
|
||||
if (this.drawingObject) {
|
||||
this.canvas.remove(this.drawingObject);
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
}
|
||||
// 清除已有选区
|
||||
else if (this.selectionObject) {
|
||||
if (this.commandManager) {
|
||||
this.commandManager.execute(
|
||||
new ClearSelectionCommand({
|
||||
selectionManager: this,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.clearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听
|
||||
this.canvas.on("mouse:down", this._mouseDownHandler);
|
||||
this.canvas.on("mouse:move", this._mouseMoveHandler);
|
||||
this.canvas.on("mouse:up", this._mouseUpHandler);
|
||||
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener("keydown", this._keyDownHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理事件监听
|
||||
*/
|
||||
cleanupEvents() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 移除事件监听
|
||||
if (this._mouseDownHandler) {
|
||||
this.canvas.off("mouse:down", this._mouseDownHandler);
|
||||
this._mouseDownHandler = null;
|
||||
}
|
||||
if (this._mouseMoveHandler) {
|
||||
this.canvas.off("mouse:move", this._mouseMoveHandler);
|
||||
this._mouseMoveHandler = null;
|
||||
}
|
||||
if (this._mouseUpHandler) {
|
||||
this.canvas.off("mouse:up", this._mouseUpHandler);
|
||||
this._mouseUpHandler = null;
|
||||
}
|
||||
if (this._keyDownHandler) {
|
||||
document.removeEventListener("keydown", this._keyDownHandler);
|
||||
this._keyDownHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选区对象
|
||||
* @returns {Object} 选区对象
|
||||
*/
|
||||
getSelectionObject() {
|
||||
return this.selectionObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选区路径
|
||||
* @returns {Array|String} 选区路径数据
|
||||
*/
|
||||
getSelectionPath() {
|
||||
return this.selectionPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取羽化值
|
||||
* @returns {Number} 羽化值
|
||||
*/
|
||||
getFeatherAmount() {
|
||||
return this.featherAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置羽化值
|
||||
* @param {Number} amount 羽化值
|
||||
*/
|
||||
setFeatherAmount(amount) {
|
||||
this.featherAmount = amount;
|
||||
return this.updateSelectionAppearance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区对象
|
||||
* @param {Object} object 选区对象
|
||||
*/
|
||||
setSelectionObject(object) {
|
||||
// 如果已存在选区,先移除
|
||||
if (this.selectionObject) {
|
||||
this.removeSelectionFromCanvas();
|
||||
}
|
||||
|
||||
// 更新选区对象
|
||||
this.selectionObject = object;
|
||||
this.selectionPath = object.path;
|
||||
this.selectionId = object.id || generateId();
|
||||
|
||||
// 更新外观
|
||||
this.updateSelectionAppearance();
|
||||
|
||||
// 添加到画布(确保在顶层)
|
||||
if (this.canvas && this.selectionObject) {
|
||||
this.canvas.add(this.selectionObject);
|
||||
this.canvas.bringToFront(this.selectionObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
// 触发选区变化回调
|
||||
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
|
||||
this.onSelectionChanged();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从路径数据设置选区
|
||||
* @param {Array|String} path 选区路径数据
|
||||
*/
|
||||
setSelectionFromPath(path) {
|
||||
if (!path) return false;
|
||||
|
||||
// 创建选区对象
|
||||
const selectionObj = new fabric.Path(path, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
});
|
||||
|
||||
// 设置选区
|
||||
return this.setSelectionObject(selectionObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选区外观
|
||||
*/
|
||||
updateSelectionAppearance() {
|
||||
if (!this.selectionObject) return false;
|
||||
|
||||
// 应用基本样式
|
||||
Object.assign(this.selectionObject, this.selectionStyle);
|
||||
|
||||
// 应用羽化效果
|
||||
if (this.featherAmount > 0) {
|
||||
this.selectionObject.shadow = new fabric.Shadow({
|
||||
color: "rgba(0, 150, 255, 0.5)",
|
||||
blur: this.featherAmount,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
} else {
|
||||
this.selectionObject.shadow = null;
|
||||
}
|
||||
|
||||
// 更新画布
|
||||
this.canvas.renderAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选区
|
||||
*/
|
||||
removeSelectionFromCanvas() {
|
||||
if (this.canvas && this.selectionObject) {
|
||||
this.canvas.remove(this.selectionObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选区
|
||||
*/
|
||||
clearSelection() {
|
||||
// 移除选区对象
|
||||
this.removeSelectionFromCanvas();
|
||||
|
||||
// 重置选区状态
|
||||
this.selectionObject = null;
|
||||
this.selectionPath = null;
|
||||
this.selectionId = null;
|
||||
this.featherAmount = 0;
|
||||
|
||||
// 触发选区变化回调
|
||||
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
|
||||
this.onSelectionChanged();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转选区
|
||||
*/
|
||||
async invertSelection() {
|
||||
if (!this.canvas || !this.selectionObject) return false;
|
||||
|
||||
// 获取画布范围
|
||||
const canvasRect = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.canvas.width,
|
||||
height: this.canvas.height,
|
||||
selectable: false,
|
||||
});
|
||||
|
||||
// 创建反选路径
|
||||
let invertedPath;
|
||||
try {
|
||||
invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path);
|
||||
} catch (error) {
|
||||
console.error("无法反转选区:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置新的选区
|
||||
const newSelection = new fabric.Path(invertedPath.path, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
});
|
||||
|
||||
return this.setSelectionObject(newSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到选区
|
||||
* @param {Object} newSelection 要添加的选区对象
|
||||
*/
|
||||
async addToSelection(newSelection) {
|
||||
if (!this.canvas) return false;
|
||||
|
||||
// 如果当前没有选区,直接使用新选区
|
||||
if (!this.selectionObject) {
|
||||
return this.setSelectionObject(newSelection);
|
||||
}
|
||||
|
||||
// 合并选区
|
||||
let combinedPath;
|
||||
try {
|
||||
combinedPath = this.selectionObject.union(newSelection);
|
||||
} catch (error) {
|
||||
console.error("无法添加到选区:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置新的选区
|
||||
const combinedSelection = new fabric.Path(combinedPath.path, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
});
|
||||
|
||||
return this.setSelectionObject(combinedSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从选区中移除
|
||||
* @param {Object} removeSelection 要移除的选区对象
|
||||
*/
|
||||
async removeFromSelection(removeSelection) {
|
||||
if (!this.canvas || !this.selectionObject) return false;
|
||||
|
||||
// 从当前选区中减去新选区
|
||||
let resultPath;
|
||||
try {
|
||||
resultPath = this.selectionObject.subtract(removeSelection);
|
||||
} catch (error) {
|
||||
console.error("无法从选区中移除:", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置新的选区
|
||||
const newSelection = new fabric.Path(resultPath.path, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
});
|
||||
|
||||
return this.setSelectionObject(newSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用羽化效果
|
||||
* @param {Number} amount 羽化值
|
||||
*/
|
||||
async featherSelection(amount) {
|
||||
if (!this.selectionObject) return false;
|
||||
|
||||
// 更新羽化值
|
||||
this.featherAmount = amount;
|
||||
|
||||
// 更新选区外观
|
||||
return this.updateSelectionAppearance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否在选区内
|
||||
* @param {Object} object 要检查的对象
|
||||
* @returns {Boolean} 是否在选区内
|
||||
*/
|
||||
isObjectInSelection(object) {
|
||||
if (!this.selectionObject || !object) return false;
|
||||
|
||||
// 获取对象的边界框
|
||||
const bounds = object.getBoundingRect();
|
||||
const { left, top, width, height } = bounds;
|
||||
|
||||
// 检查对象的中心点和四个角是否在选区内
|
||||
const centerX = left + width / 2;
|
||||
const centerY = top + height / 2;
|
||||
|
||||
// 检查中心点
|
||||
if (this.isPointInSelection(centerX, centerY)) return true;
|
||||
|
||||
// 检查四个角
|
||||
if (this.isPointInSelection(left, top)) return true;
|
||||
if (this.isPointInSelection(left + width, top)) return true;
|
||||
if (this.isPointInSelection(left, top + height)) return true;
|
||||
if (this.isPointInSelection(left + width, top + height)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查点是否在选区内
|
||||
* @param {Number} x X坐标
|
||||
* @param {Number} y Y坐标
|
||||
* @returns {Boolean} 是否在选区内
|
||||
*/
|
||||
isPointInSelection(x, y) {
|
||||
if (!this.selectionObject) return false;
|
||||
|
||||
// 使用fabric.js的containsPoint方法判断点是否在选区内
|
||||
return this.selectionObject.containsPoint({ x, y });
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自由选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
startFreeSelection(options) {
|
||||
if (!this.canvas || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
this.startPoint = pointer;
|
||||
|
||||
// 创建用于绘制轨迹的点数组
|
||||
this.drawingPoints = [pointer];
|
||||
|
||||
// 初始化SVG路径字符串
|
||||
this.currentPathString = `M ${pointer.x} ${pointer.y}`;
|
||||
|
||||
// 创建临时路径对象用于实时显示
|
||||
this.drawingObject = new fabric.Path(this.currentPathString, {
|
||||
stroke: this.selectionStyle.stroke,
|
||||
strokeWidth: this.selectionStyle.strokeWidth,
|
||||
strokeDashArray: this.selectionStyle.strokeDashArray,
|
||||
fill: "transparent",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
strokeLineCap: "round",
|
||||
strokeLineJoin: "round",
|
||||
});
|
||||
|
||||
// 添加到画布
|
||||
this.canvas.add(this.drawingObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制自由选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
drawFreeSelection(options) {
|
||||
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
|
||||
// 添加新的点,但避免添加过于密集的点
|
||||
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(pointer.x - lastPoint.x, 2) + Math.pow(pointer.y - lastPoint.y, 2)
|
||||
);
|
||||
|
||||
// 只有当距离大于2像素时才添加新点,避免路径过于复杂
|
||||
if (distance > 2) {
|
||||
this.drawingPoints.push(pointer);
|
||||
|
||||
// 更新路径字符串
|
||||
this.currentPathString += ` L ${pointer.x} ${pointer.y}`;
|
||||
|
||||
// 移除旧的绘制对象
|
||||
this.canvas.remove(this.drawingObject);
|
||||
|
||||
// 创建新的路径对象
|
||||
this.drawingObject = new fabric.Path(this.currentPathString, {
|
||||
stroke: this.selectionStyle.stroke,
|
||||
strokeWidth: this.selectionStyle.strokeWidth,
|
||||
strokeDashArray: this.selectionStyle.strokeDashArray,
|
||||
fill: "transparent",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
strokeLineCap: "round",
|
||||
strokeLineJoin: "round",
|
||||
});
|
||||
|
||||
// 重新添加到画布
|
||||
this.canvas.add(this.drawingObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束自由选区
|
||||
*/
|
||||
endFreeSelection() {
|
||||
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
|
||||
|
||||
// 检查是否有足够的点来形成选区
|
||||
if (this.drawingPoints.length < 3) {
|
||||
// 点太少,清除绘制对象
|
||||
this.canvas.remove(this.drawingObject);
|
||||
this.drawingObject = null;
|
||||
this.drawingPoints = null;
|
||||
this.startPoint = null;
|
||||
this.currentPathString = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动闭合路径 - 连接最后一点到第一点
|
||||
const firstPoint = this.drawingPoints[0];
|
||||
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
|
||||
const closingDistance = Math.sqrt(
|
||||
Math.pow(firstPoint.x - lastPoint.x, 2) + Math.pow(firstPoint.y - lastPoint.y, 2)
|
||||
);
|
||||
|
||||
// 如果首尾距离较大,自动添加闭合线段
|
||||
let finalPathString = this.currentPathString;
|
||||
if (closingDistance > 10) {
|
||||
finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`;
|
||||
}
|
||||
finalPathString += " Z"; // 闭合路径
|
||||
|
||||
// 创建最终选区对象
|
||||
const selectionObj = new fabric.Path(finalPathString, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
fill: this.selectionStyle.fill, // 恢复填充
|
||||
});
|
||||
|
||||
// 移除绘制中的临时对象
|
||||
this.canvas.remove(this.drawingObject);
|
||||
|
||||
// 重置绘制状态
|
||||
this.drawingObject = null;
|
||||
this.drawingPoints = null;
|
||||
this.startPoint = null;
|
||||
this.currentPathString = null;
|
||||
|
||||
// 设置选区
|
||||
this.setSelectionObject(selectionObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始矩形选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
startRectangleSelection(options) {
|
||||
if (!this.canvas || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
this.startPoint = pointer;
|
||||
|
||||
// 创建矩形对象
|
||||
this.drawingObject = new fabric.Rect({
|
||||
left: pointer.x,
|
||||
top: pointer.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
...this.selectionStyle,
|
||||
fill: "transparent", // 在绘制过程中不显示填充
|
||||
});
|
||||
|
||||
// 添加到画布
|
||||
this.canvas.add(this.drawingObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制矩形选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
drawRectangleSelection(options) {
|
||||
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
|
||||
// 计算宽度和高度
|
||||
const width = Math.abs(pointer.x - this.startPoint.x);
|
||||
const height = Math.abs(pointer.y - this.startPoint.y);
|
||||
|
||||
// 确定左上角坐标
|
||||
const left = Math.min(this.startPoint.x, pointer.x);
|
||||
const top = Math.min(this.startPoint.y, pointer.y);
|
||||
|
||||
// 更新矩形
|
||||
this.drawingObject.set({
|
||||
left: left,
|
||||
top: top,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束矩形选区
|
||||
*/
|
||||
endRectangleSelection() {
|
||||
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
||||
|
||||
// 将矩形转换为路径
|
||||
const left = this.drawingObject.left;
|
||||
const top = this.drawingObject.top;
|
||||
const width = this.drawingObject.width;
|
||||
const height = this.drawingObject.height;
|
||||
|
||||
// 如果矩形太小,忽略
|
||||
if (width < 5 || height < 5) {
|
||||
this.canvas.remove(this.drawingObject);
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建矩形路径字符串
|
||||
const pathString = `M ${left} ${top} L ${left + width} ${top} L ${
|
||||
left + width
|
||||
} ${top + height} L ${left} ${top + height} Z`;
|
||||
|
||||
// 创建最终选区对象
|
||||
const selectionObj = new fabric.Path(pathString, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
fill: this.selectionStyle.fill, // 恢复填充
|
||||
});
|
||||
|
||||
// 移除绘制中的临时对象
|
||||
this.canvas.remove(this.drawingObject);
|
||||
|
||||
// 重置绘制状态
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
|
||||
// 设置选区
|
||||
this.setSelectionObject(selectionObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始椭圆选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
startEllipseSelection(options) {
|
||||
if (!this.canvas || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
this.startPoint = pointer;
|
||||
|
||||
// 创建椭圆对象
|
||||
this.drawingObject = new fabric.Ellipse({
|
||||
left: pointer.x,
|
||||
top: pointer.y,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
...this.selectionStyle,
|
||||
fill: "transparent", // 在绘制过程中不显示填充
|
||||
// originX: "left",
|
||||
// originY: "top",
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
// 添加到画布
|
||||
this.canvas.add(this.drawingObject);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制椭圆选区
|
||||
* @param {Object} options 事件对象
|
||||
*/
|
||||
drawEllipseSelection(options) {
|
||||
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
||||
|
||||
// 获取鼠标位置
|
||||
const pointer = this.canvas.getPointer(options.e);
|
||||
|
||||
// 计算半径
|
||||
const rx = Math.abs(pointer.x - this.startPoint.x) / 2;
|
||||
const ry = Math.abs(pointer.y - this.startPoint.y) / 2;
|
||||
|
||||
// 确定中心坐标
|
||||
const left = Math.min(this.startPoint.x, pointer.x);
|
||||
const top = Math.min(this.startPoint.y, pointer.y);
|
||||
|
||||
// 更新椭圆
|
||||
this.drawingObject.set({
|
||||
left: left,
|
||||
top: top,
|
||||
rx: rx,
|
||||
ry: ry,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束椭圆选区
|
||||
*/
|
||||
endEllipseSelection() {
|
||||
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
|
||||
|
||||
// 获取椭圆参数
|
||||
const { left, top, rx, ry } = this.drawingObject;
|
||||
|
||||
// 如果椭圆太小,忽略
|
||||
if (rx < 2 || ry < 2) {
|
||||
this.canvas.remove(this.drawingObject);
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算中心点
|
||||
const cx = left + rx;
|
||||
const cy = top + ry;
|
||||
|
||||
// 将椭圆转换为路径字符串
|
||||
const pathString = this.ellipseToSVGPath(cx, cy, rx, ry);
|
||||
|
||||
// 创建最终选区对象
|
||||
const selectionObj = new fabric.Path(pathString, {
|
||||
...this.selectionStyle,
|
||||
id: `selection_${Date.now()}`,
|
||||
name: "selection",
|
||||
fill: this.selectionStyle.fill, // 恢复填充
|
||||
});
|
||||
|
||||
// 移除绘制中的临时对象
|
||||
this.canvas.remove(this.drawingObject);
|
||||
|
||||
// 重置绘制状态
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
|
||||
// 设置选区
|
||||
this.setSelectionObject(selectionObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将椭圆转换为SVG路径字符串
|
||||
* @param {Number} cx 中心点X坐标
|
||||
* @param {Number} cy 中心点Y坐标
|
||||
* @param {Number} rx X半径
|
||||
* @param {Number} ry Y半径
|
||||
* @returns {String} SVG路径字符串
|
||||
*/
|
||||
ellipseToSVGPath(cx, cy, rx, ry) {
|
||||
// 使用椭圆弧命令创建完整椭圆
|
||||
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${
|
||||
cx + rx
|
||||
} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区工具
|
||||
* @param {string} type 选区类型:OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE
|
||||
*/
|
||||
setSelectionType(type) {
|
||||
this.selectionType = type;
|
||||
|
||||
// 如果正在绘制,清除临时对象
|
||||
if (this.drawingObject) {
|
||||
this.canvas.remove(this.drawingObject);
|
||||
this.drawingObject = null;
|
||||
this.startPoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区工具的鼠标事件
|
||||
*/
|
||||
setupSelectionEvents() {
|
||||
// 选区事件现在通过 setCurrentTool 方法管理
|
||||
// 这个方法现在主要用于刷新或重置事件监听
|
||||
if (!this.canvas || !this.isActive) return;
|
||||
|
||||
// 确保选区处于激活状态
|
||||
if (this.tools.includes(this.currentTool)) {
|
||||
this.isActive = true;
|
||||
// 如果事件还没有初始化,初始化它们
|
||||
if (!this._mouseDownHandler) {
|
||||
this.initEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this.cleanupEvents();
|
||||
this.clearSelection();
|
||||
this.canvas = null;
|
||||
this.commandManager = null;
|
||||
this.layerManager = null;
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,12 @@ export class ThumbnailManager {
|
||||
// 重新创建遮罩对象
|
||||
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
|
||||
|
||||
clippingMaskFabricObject.clipPath = null;
|
||||
// clippingMaskFabricObject.clipPath = null;
|
||||
clippingMaskFabricObject.set({
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
clippingMaskFabricObject.dirty = true;
|
||||
// clippingMaskFabricObject.dirty = true;
|
||||
clippingMaskFabricObject.setCoords();
|
||||
}
|
||||
|
||||
@@ -128,8 +128,13 @@ export class ThumbnailManager {
|
||||
}
|
||||
|
||||
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
||||
let layersToRasterize = [];
|
||||
|
||||
if (!layer) {
|
||||
console.warn("⚠️ 无效的图层,无法收集对象");
|
||||
return [];
|
||||
}
|
||||
|
||||
let layersToRasterize = [];
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
// 组图层:收集自身和所有子图层
|
||||
layersToRasterize = this._collectLayersToRasterize(layer);
|
||||
|
||||
@@ -67,6 +67,12 @@ export class ToolManager {
|
||||
|
||||
// 工具列表 - 与OperationType保持一致
|
||||
this.tools = {
|
||||
// 禁用工具
|
||||
[OperationType.DISABLED]: {
|
||||
name: "禁用工具",
|
||||
icon: "disabled",
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
// 基础工具
|
||||
[OperationType.SELECT]: {
|
||||
name: "选择工具",
|
||||
@@ -83,6 +89,7 @@ export class ToolManager {
|
||||
shortcut: "B",
|
||||
setup: this.setupBrushTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.ERASER]: {
|
||||
name: "橡皮擦",
|
||||
@@ -91,6 +98,7 @@ export class ToolManager {
|
||||
shortcut: "E",
|
||||
setup: this.setupEraserTool.bind(this),
|
||||
allowedInRedGreen: true, // 红绿图模式允许橡皮擦
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.EYEDROPPER]: {
|
||||
name: "吸色工具",
|
||||
@@ -117,6 +125,7 @@ export class ToolManager {
|
||||
shortcut: "L",
|
||||
setup: this.setupLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.LASSO_RECTANGLE]: {
|
||||
name: "矩形套索工具",
|
||||
@@ -126,6 +135,7 @@ export class ToolManager {
|
||||
altKey: true,
|
||||
setup: this.setupRectangleLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.LASSO_ELLIPSE]: {
|
||||
name: "椭圆形套索工具",
|
||||
@@ -135,6 +145,7 @@ export class ToolManager {
|
||||
altKey: true,
|
||||
setup: this.setupEllipseLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
|
||||
// 选区工具 - 只需要矩形选区
|
||||
@@ -164,6 +175,7 @@ export class ToolManager {
|
||||
shortcut: "J",
|
||||
setup: this.setupLiquifyTool.bind(this),
|
||||
allowedInRedGreen: false, // 红绿图模式不允许液化
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.TEXT]: {
|
||||
name: "文本工具",
|
||||
@@ -174,6 +186,32 @@ export class ToolManager {
|
||||
allowedInRedGreen: false, // 红绿图模式不允许文本
|
||||
},
|
||||
|
||||
// 部件选取工具
|
||||
[OperationType.PART]: {
|
||||
name: "部件选取工具",
|
||||
icon: "part",
|
||||
cursor: "default",
|
||||
setup: this.setupPartTool.bind(this),
|
||||
},
|
||||
[OperationType.PART_RECTANGLE]: {
|
||||
name: "部件选取工具-矩形",
|
||||
icon: "part",
|
||||
cursor: "default",
|
||||
setup: this.setupPartTool.bind(this),
|
||||
},
|
||||
[OperationType.PART_BRUSH]: {
|
||||
name: "部件选取工具-画笔",
|
||||
icon: "part",
|
||||
cursor: "default",
|
||||
setup: this.setupPartTool.bind(this),
|
||||
},
|
||||
[OperationType.PART_ERASER]: {
|
||||
name: "部件选取工具-橡皮擦",
|
||||
icon: "part",
|
||||
cursor: "default",
|
||||
setup: this.setupPartTool.bind(this),
|
||||
},
|
||||
|
||||
// 红绿图模式专用工具
|
||||
[OperationType.RED_BRUSH]: {
|
||||
name: "红色笔刷",
|
||||
@@ -331,8 +369,9 @@ export class ToolManager {
|
||||
* @param {String} toolId 工具ID
|
||||
*/
|
||||
setTool(toolId) {
|
||||
const tool = this.tools[toolId];
|
||||
// 检查工具是否存在
|
||||
if (!this.tools[toolId]) {
|
||||
if (!tool) {
|
||||
console.error(`工具 '${toolId}' 不存在`);
|
||||
return;
|
||||
}
|
||||
@@ -348,15 +387,20 @@ export class ToolManager {
|
||||
console.warn(`工具 '${toolId}' 只能在红绿图模式下使用`);
|
||||
return;
|
||||
}
|
||||
if(tool?.specialLayerDisabled && this.checkToolCanOperateSelectedObject()){
|
||||
console.warn(`工具 '${toolId}' 不能在当前选中对象上操作`);
|
||||
toolId = OperationType.DISABLED;
|
||||
}
|
||||
|
||||
// 保存先前的工具
|
||||
// 保存先前的工具
|
||||
this.previousTool = this.activeTool.value;
|
||||
|
||||
// 取消画布的选中状态
|
||||
this.canvas?.discardActiveObject();
|
||||
this.canvasManager?.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
this.canvas?.renderAll();
|
||||
|
||||
if(toolId !== OperationType.DISABLED){
|
||||
this.canvas?.discardActiveObject();
|
||||
this.canvasManager?.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
this.canvas?.renderAll();
|
||||
}
|
||||
// 隐藏文本编辑面板
|
||||
this.hideTextEditor();
|
||||
|
||||
@@ -374,10 +418,9 @@ export class ToolManager {
|
||||
}
|
||||
|
||||
// 设置工具特定的状态
|
||||
const tool = this.tools[toolId];
|
||||
if (tool && typeof tool.setup === "function") {
|
||||
console.log(`画布切换工具:${tool.name}(${toolId})`)
|
||||
this.canvas.toolId = toolId;
|
||||
console.log(`画布切换工具:${tool.name}(${toolId})`)
|
||||
this.canvas.toolId = toolId;
|
||||
tool.setup();
|
||||
}
|
||||
|
||||
@@ -424,7 +467,7 @@ export class ToolManager {
|
||||
|
||||
const currentTool = this.activeTool.value;
|
||||
const tool = this.tools[currentTool];
|
||||
|
||||
if(tool?.specialLayerDisabled && this.checkToolCanOperateSelectedObject()) return;
|
||||
// 根据当前工具设置selection状态
|
||||
if (currentTool === OperationType.SELECT) {
|
||||
this.canvas.selection = true;
|
||||
@@ -455,14 +498,29 @@ export class ToolManager {
|
||||
if (!this.canvas) return;
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前工具是否禁止操作当前选中的对象
|
||||
* @returns {Boolean} 是否可以切换
|
||||
*/
|
||||
checkToolCanOperateSelectedObject() {
|
||||
const layer = this.layerManager?.getActiveLayer();
|
||||
const isSpecialLayer = !!layer?.isPrintTrims || !!layer?.isPrintTrimsGroup;
|
||||
if (isSpecialLayer) {
|
||||
this._disableBrushIndicator();
|
||||
this.canvas.defaultCursor = "not-allowed";
|
||||
}
|
||||
return isSpecialLayer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置画笔工具
|
||||
*/
|
||||
setupBrushTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.selection = false;
|
||||
@@ -506,6 +564,8 @@ export class ToolManager {
|
||||
*/
|
||||
setupEraserTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.selection = false;
|
||||
|
||||
@@ -558,6 +618,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupLassoTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = false;
|
||||
@@ -639,6 +700,20 @@ export class ToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置部件选取工具
|
||||
*/
|
||||
setupPartTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = false;
|
||||
|
||||
if (this.canvasManager && this.canvasManager.partManager) {
|
||||
this.canvasManager.partManager.setCurrentTool(OperationType.PART);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置波浪工具
|
||||
*/
|
||||
@@ -654,6 +729,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupLiquifyTool() {
|
||||
if (!this.canvas || !this.layerManager) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = false;
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AnimationManager {
|
||||
|
||||
// 如果变化太小,直接应用缩放
|
||||
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
||||
// this._applyZoom(point, targetZoom);
|
||||
this._applyZoom(point, targetZoom);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class AnimationManager {
|
||||
this._zoomAnimation = null;
|
||||
|
||||
// 确保最终状态准确
|
||||
// this._applyZoom(point, targetZoom, true);
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ export class AnimationManager {
|
||||
this._zoomAnimation = null;
|
||||
|
||||
// 确保最终状态准确
|
||||
// this._applyZoom(point, targetZoom, true);
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -817,7 +817,7 @@ export class AnimationManager {
|
||||
this._wasZooming = false;
|
||||
|
||||
// 确保最终状态准确
|
||||
// this._applyZoom(point, targetZoom, true);
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PerformanceManager } from "./PerformanceManager.js";
|
||||
*/
|
||||
export class CommandManager {
|
||||
constructor(options = {}) {
|
||||
this.canvas = options.canvas;
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
this.maxHistorySize = options.maxHistorySize || 50;
|
||||
@@ -205,6 +206,7 @@ export class CommandManager {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
this.canvas?.discardActiveObject();
|
||||
const command = this.undoStack.pop();
|
||||
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
|
||||
|
||||
@@ -243,6 +245,7 @@ export class CommandManager {
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
this.canvas?.discardActiveObject();
|
||||
const command = this.redoStack.pop();
|
||||
console.log(`↪️ 重做命令: ${command.constructor.name}`);
|
||||
|
||||
|
||||
@@ -688,7 +688,6 @@ export class CanvasEventManager {
|
||||
this.layerManager.commandManager.execute(transformCmd, {
|
||||
name: "对象修改",
|
||||
});
|
||||
|
||||
// 清除临时状态记录
|
||||
delete activeObj._initialTransformState;
|
||||
}
|
||||
|
||||