41 Commits

Author SHA1 Message Date
wxd
967996429b 更新 .gitea/workflows/prod_build_schedule.yaml 2026-03-23 10:45:31 +08:00
wxd
f88129f8a9 更新 .gitea/workflows/prod_build_schedule.yaml 2026-03-13 14:02:26 +08:00
wxd
02ccd546bc 更新 .gitea/workflows/prod_build_schedule.yaml 2026-03-06 18:45:13 +08:00
wxd
4006e7f1c1 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m6s
2026-03-05 10:59:08 +08:00
wxd
352f2b7bae 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m22s
2026-03-02 09:52:19 +08:00
wxd
1839bae545 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m25s
2026-02-24 11:38:54 +08:00
wxd
93cb27238b 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m54s
2026-02-06 14:18:58 +08:00
wxd
a0de3ce96d 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m51s
2026-02-06 10:37:16 +08:00
wxd
989c8468f0 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m9s
2026-02-05 16:44:03 +08:00
wxd
f8a864d740 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m55s
2026-02-04 15:59:53 +08:00
wxd
afde6d2024 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m49s
2026-02-03 09:40:44 +08:00
fe4b39ac97 更新 .gitea/workflows/research_build_manual.yaml 2026-02-02 10:34:19 +08:00
c25e5042dd 上传文件至「.gitea/workflows」 2026-02-02 10:33:11 +08:00
wxd
27849503b3 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m56s
2026-01-29 16:56:04 +08:00
wxd
5fdf071510 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m48s
2026-01-29 16:32:36 +08:00
wxd
ff3e62506c 更新 .gitea/workflows/prod_build_schedule.yaml 2026-01-29 16:32:27 +08:00
wxd
24accf803d 更新 .gitea/workflows/prod_build_schedule.yaml 2026-01-29 09:55:57 +08:00
wxd
b5dcc80759 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 1m55s
2026-01-28 16:36:56 +08:00
wxd
a294116696 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m8s
2026-01-28 10:06:12 +08:00
wxd
d45f0b0ecd 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m1s
2026-01-27 14:18:56 +08:00
wxd
2a522e06a0 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m46s
2026-01-27 10:16:57 +08:00
wxd
af3bff6d80 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 5m20s
2026-01-26 16:45:17 +08:00
wxd
d9d57066fc 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m59s
2026-01-24 11:56:59 +08:00
wxd
e34986d09d 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m5s
2026-01-23 22:32:58 +08:00
wxd
57adf91646 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m5s
2026-01-23 21:44:02 +08:00
wxd
016c1de922 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m53s
2026-01-23 15:46:05 +08:00
wxd
a167d3f2ba 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m55s
2026-01-21 16:59:55 +08:00
wxd
fb7bf53680 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m47s
2026-01-20 16:49:28 +08:00
wxd
cb453297be 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m55s
2026-01-15 10:52:10 +08:00
wxd
a2f4f946ac 更新 .gitea/workflows/prod_build_schedule.yaml 2026-01-14 17:10:29 +08:00
67d5bb6874 2025.12.19 生产部署
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m35s
2025-12-19 17:47:22 +08:00
db20117500 2025.12.17 生产部署
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 3m32s
2025-12-17 14:03:27 +08:00
f3e4408dc0 更新 .gitea/workflows/prod_build_schedule.yaml
All checks were successful
AiDA WEB-Node.js 生产分支构建部署 / build (18.18.0) (push) Successful in 2m14s
2025-12-16 21:50:38 +08:00
c2d13187f0 2025.12.16 生产部署 2025-12-16 17:24:46 +08:00
7155dedc8d 更新 .gitea/workflows/prod_build_schedule.yaml 2025-12-16 17:23:32 +08:00
de8a6b9dc7 上传文件至「.gitea/workflows」 2025-12-01 17:13:08 +08:00
9c562143da 更新 .gitea/workflows/prod_build_manual.yaml 2025-11-29 00:07:07 +08:00
21fb901580 更新 .gitea/workflows/prod_build_schedule.yaml 2025-11-29 00:06:19 +08:00
29eb464772 更新 .gitea/workflows/prod_build_schedule.yaml
Some checks failed
AiDA WEB-Node.js StableVersion 分支构建部署 / build (18.18.0) (push) Failing after 2m34s
2025-11-28 17:36:57 +08:00
6f5d696c2d 更新 .gitea/workflows/prod_build_schedule.yaml 2025-11-28 17:35:50 +08:00
14a3e467f8 更新 .gitea/workflows/develop_build_commit.yaml 2025-11-28 16:07:40 +08:00
123 changed files with 3880 additions and 8679 deletions

View File

@@ -0,0 +1,90 @@
name: git commit 控制 AiDA WEB-Node.js 开发分支构建部署
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: 0.记录开始时间
id: build_start_time
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: dev_vite
- 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 重载命令已发送。"
- name: 5.发送构建结果邮件
if: always() # 无论上一步是否失败,都执行此步骤
uses: dawidd6/action-send-mail@v3
with:
from: ${{ secrets.MAIL_USERNAME }}
# --- 邮件配置 ---
server_address: smtp.gmail.com # 替换为你的SMTP服务器地址
server_port: 465 # 替换为你的SMTP端口 (通常是465或587)
username: ${{ secrets.MAIL_USERNAME }} # 存储在Secrets中的邮箱用户名
password: ${{ secrets.MAIL_PASSWORD }} # 存储在Secrets中的邮箱密码
subject: 'Gitea Actions 构建通知: ${{ job.status }} - AiDA back-java Develop'
# 收件人列表,可以根据需要更改
to: 'xupei3360@163.com,txli@aidlab.hk,cgzhou@aidlab.hk,zchengrong@yeah.net' # 替换为实际收件人邮箱
# --- 邮件正文内容 ---
body: |
项目: AiDA back-java Develop
分支: dev/3.1_release_merge
🎉 构建结果: ${{ job.status }}
📅 构建时间: ${{ steps.build_start_time.outputs.current_time }}
🔗 构建链接: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
# 确保邮件内容为纯文本,或者你可以设置为 html: true 并调整 body
content_type: text/plain

View File

@@ -0,0 +1,85 @@
name: 手动触发 AiDA WEB-Node.js 开发分支构建部署
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: 0.记录开始时间
id: build_start_time
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: dev_vite
- 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 重载命令已发送。"
- name: 5.发送构建结果邮件
if: always() # 无论上一步是否失败,都执行此步骤
uses: dawidd6/action-send-mail@v3
with:
from: ${{ secrets.MAIL_USERNAME }}
# --- 邮件配置 ---
server_address: smtp.gmail.com # 替换为你的SMTP服务器地址
server_port: 465 # 替换为你的SMTP端口 (通常是465或587)
username: ${{ secrets.MAIL_USERNAME }} # 存储在Secrets中的邮箱用户名
password: ${{ secrets.MAIL_PASSWORD }} # 存储在Secrets中的邮箱密码
subject: 'Gitea Actions 构建通知: ${{ job.status }} - AiDA back-java Develop'
# 收件人列表,可以根据需要更改
to: 'xupei3360@163.com,txli@aidlab.hk,cgzhou@aidlab.hk,zchengrong@yeah.net' # 替换为实际收件人邮箱
# --- 邮件正文内容 ---
body: |
项目: AiDA back-java Develop
分支: dev/3.1_release_merge
🎉 构建结果: ${{ job.status }}
📅 构建时间: ${{ steps.build_start_time.outputs.current_time }}
🔗 构建链接: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
# 确保邮件内容为纯文本,或者你可以设置为 html: true 并调整 body
content_type: text/plain

View File

@@ -0,0 +1,78 @@
name: AiDA WEB-Node.js 生产分支构建部署
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 18.18.0 ]
steps:
- name: 0.记录开始时间
id: build_start_time
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: StableVersion
- name: 2.设置 Node.js 环境
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.发送构建结果邮件
if: always() # 无论上一步是否失败,都执行此步骤
uses: dawidd6/action-send-mail@v3
with:
from: ${{ secrets.MAIL_USERNAME }}
# --- 邮件配置 ---
server_address: smtp.gmail.com # 替换为你的SMTP服务器地址
server_port: 465 # 替换为你的SMTP端口 (通常是465或587)
username: ${{ secrets.MAIL_USERNAME }} # 存储在Secrets中的邮箱用户名
password: ${{ secrets.MAIL_PASSWORD }} # 存储在Secrets中的邮箱密码
subject: 'Gitea Actions 构建通知: ${{ job.status }} - AiDA back-java Develop'
# 收件人列表,可以根据需要更改
to: 'xupei3360@163.com,txli@aidlab.hk,cgzhou@aidlab.hk,zchengrong@yeah.net' # 替换为实际收件人邮箱
# --- 邮件正文内容 ---
body: |
项目: AiDA back-java Develop
分支: dev/3.1_release_merge
🎉 构建结果: ${{ job.status }}
📅 构建时间: ${{ steps.build_start_time.outputs.current_time }}
🔗 构建链接: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
# 确保邮件内容为纯文本,或者你可以设置为 html: true 并调整 body
content_type: text/plain

View File

@@ -0,0 +1,81 @@
name: AiDA WEB-Node.js 生产分支构建部署
on:
schedule:
# cron为UTC时区构建时间=部署时间-8小时 {*分 (-8)时 *日 *月 *周} ---
# 示例: 1月1日22点22分触发构建 cron写作 - '22 14 1 1 *'
- cron: '00 14 23 3 *'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 18.18.0 ]
steps:
- name: 0.记录开始时间
id: build_start_time
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: StableVersion
- name: 2.设置 Node.js 环境
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.发送构建结果邮件
if: always() # 无论上一步是否失败,都执行此步骤
uses: dawidd6/action-send-mail@v3
with:
from: ${{ secrets.MAIL_USERNAME }}
# --- 邮件配置 ---
server_address: smtp.gmail.com # 替换为你的SMTP服务器地址
server_port: 465 # 替换为你的SMTP端口 (通常是465或587)
username: ${{ secrets.MAIL_USERNAME }} # 存储在Secrets中的邮箱用户名
password: ${{ secrets.MAIL_PASSWORD }} # 存储在Secrets中的邮箱密码
subject: 'Gitea Actions 构建通知: ${{ job.status }} - AiDA back-java Develop'
# 收件人列表,可以根据需要更改
to: 'cgzhou@aidlab.hk,zchengrong@yeah.net' # 替换为实际收件人邮箱
# --- 邮件正文内容 ---
body: |
项目: AiDA WEB-Node.js 生产分支构建部署
分支: StableVersion
🎉 构建结果: ${{ job.status }}
📅 构建时间: ${{ steps.build_start_time.outputs.current_time }}
🔗 构建链接: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
# 确保邮件内容为纯文本,或者你可以设置为 html: true 并调整 body
content_type: text/plain

View File

@@ -0,0 +1,85 @@
name: 手动触发 AiDA WEB-Node.js 开发分支构建部署
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 18.18.0 ]
env:
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/Research/research-aida-web-front
steps:
- name: 0.记录开始时间
id: build_start_time
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
- name: 1.检出代码
uses: actions/checkout@v4
with:
ref: research
- 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 重载命令已发送。"
- name: 5.发送构建结果邮件
if: always() # 无论上一步是否失败,都执行此步骤
uses: dawidd6/action-send-mail@v3
with:
from: ${{ secrets.MAIL_USERNAME }}
# --- 邮件配置 ---
server_address: smtp.gmail.com # 替换为你的SMTP服务器地址
server_port: 465 # 替换为你的SMTP端口 (通常是465或587)
username: ${{ secrets.MAIL_USERNAME }} # 存储在Secrets中的邮箱用户名
password: ${{ secrets.MAIL_PASSWORD }} # 存储在Secrets中的邮箱密码
subject: 'Gitea Actions 构建通知: ${{ job.status }} - AiDA back-java Develop'
# 收件人列表,可以根据需要更改
to: 'xupei3360@163.com,txli@aidlab.hk,cgzhou@aidlab.hk,zchengrong@yeah.net' # 替换为实际收件人邮箱
# --- 邮件正文内容 ---
body: |
项目: AiDA back-java Develop
分支: dev/3.1_release_merge
🎉 构建结果: ${{ job.status }}
📅 构建时间: ${{ steps.build_start_time.outputs.current_time }}
🔗 构建链接: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
# 确保邮件内容为纯文本,或者你可以设置为 html: true 并调整 body
content_type: text/plain

BIN
dist.7z Normal file

Binary file not shown.

View File

@@ -1,53 +0,0 @@
name: AiDA WEB-Node.js StableVersion 分支构建部署
on:
workflow_dispatch:
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 任务完成。"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

View File

@@ -1,16 +1,7 @@
<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;
@@ -18,19 +9,7 @@
-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;

View File

@@ -54,24 +54,6 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe7a4;</span>
<div class="name">混合模式</div>
<div class="code-name">&amp;#xe7a4;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60f;</span>
<div class="name">更多</div>
<div class="code-name">&amp;#xe60f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe8d7;</span>
<div class="name">平铺</div>
<div class="code-name">&amp;#xe8d7;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe650;</span>
<div class="name">裁剪</div>
@@ -294,9 +276,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
url('iconfont.woff?t=1766460927921') format('woff'),
url('iconfont.ttf?t=1766460927921') format('truetype');
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
url('iconfont.woff?t=1762934152017') format('woff'),
url('iconfont.ttf?t=1762934152017') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -322,33 +304,6 @@
<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">
@@ -682,30 +637,6 @@
<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>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4292253 */
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
url('iconfont.woff?t=1766460927921') format('woff'),
url('iconfont.ttf?t=1766460927921') format('truetype');
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
url('iconfont.woff?t=1762934152017') format('woff'),
url('iconfont.ttf?t=1762934152017') format('truetype');
}
.iconfont {
@@ -13,18 +13,6 @@
-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";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,27 +5,6 @@
"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": "裁剪",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,10 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

View File

@@ -1,10 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 997 B

View File

@@ -1,10 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -1250,14 +1250,10 @@ 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: 0.9em !important;
height: 0.9em !important;
width: 9px !important;
height: 9px !important;
}
.ant-spin {
color: #000;
@@ -1362,7 +1358,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;
min-width: 13rem;
width: 13rem;
}
.admin_page .admin_state_item > span > span {
color: red;

View File

@@ -1378,14 +1378,10 @@ 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: .9em !important;
height: .9em !important;
width: 9px !important;
height: 9px !important;
}
.ant-spin{
color: #000;
@@ -1494,7 +1490,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
align-items: center;
>span{
white-space: nowrap;
min-width: 13rem;
width: 13rem;
>span{
color: red;
}

View File

@@ -17,8 +17,7 @@
<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"
@@ -50,10 +49,7 @@
</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')"
@@ -62,10 +58,7 @@
/>
</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')"
@@ -74,10 +67,7 @@
/>
</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"
@@ -96,19 +86,6 @@
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>
@@ -119,7 +96,7 @@
<a-spin size="large" />
</div>
</template>
<script lang="ts">
<script>
import {
defineComponent,
ref,
@@ -128,114 +105,90 @@ import {
onMounted,
nextTick,
toRefs,
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'
} 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: {},
props: {
planOptions: {
type: Array,
default: () => []
}
},
emits: ['searchHistoryList'],
emits: ["searchHistoryList"],
setup(props, { emit }) {
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')
})
const {t} = useI18n()
let operations = reactive({
operationsModal: false,
operationsEdit: false,
loadingShow: false,
title: null
})
title: null,
});
let operationsData = reactive({
accountId: -1,
userName: '',
userEmail: '',
password: '',
oldPassword: '',
credits: '',
subscriptionPlanId: '',
oldSubscriptionPlanId: ''
})
userName: "",
userEmail: "",
password: "",
oldPassword: "",
credits: "",
});
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.subscriptionPlanId = data.subscriptionPlanId || ''
operationsData.oldSubscriptionPlanId = data.subscriptionPlanId || ''
operationsData.credits = data.creditsUsageLimit;
// operationsData.accountId = data.accountId
// operationsData.userName = data.userName
// operationsData.userEmail = data.userEmail
// operationsData.validStartTime = formatTime(data.validStartTime)
// operationsData.validEndTime = formatTime(data.validEndTime)
}
if (funStr.value == 'Add') {
operationsData.subscriptionPlanId = ''
operationsData.oldSubscriptionPlanId = ''
}
}
let focus = event => {
};
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,
@@ -245,63 +198,57 @@ export default defineComponent({
userPassword:
operationsData.password == operationsData.oldPassword
? null
: md5(operationsData.password + 'abc'),
subscriptionPlanId: operationsData.subscriptionPlanId
}
}
: md5(operationsData.password + "abc"),
};
};
let cancelDsign = () => {
operationsData.accountId = -1
operationsData.userName = ''
operationsData.userEmail = ''
operationsData.password = ''
operationsData.credits = ''
operationsData.subscriptionPlanId = ''
operationsData.oldSubscriptionPlanId = ''
operations.operationsModal = false
}
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()
let data;
if (operations.title?.value == "Add") {
data = setAddData();
if (!isEmail(data.userEmail)) {
message.info(t('admin.jsContent1'))
return
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
}
})
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");
}
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");
}
}
);
}
}
};
return {
...toRefs(operations),
...toRefs(operationsData),
@@ -311,16 +258,14 @@ export default defineComponent({
focus,
blur,
setOk,
planOptions,
activePlanOptions
}
};
},
data() {
return {}
return {};
},
mounted() {},
methods: {}
})
methods: {},
});
</script>
<style lang="less" scoped>
:deep(.allUserPoeration_modal) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
layer.clippingMask,
this.canvas
);
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位

View File

@@ -1,306 +0,0 @@
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;
}
console.log("===========", object.toObject(["id", "layerId", "layerName"]))
this.oldObjects = object;
const img = await new Promise((resolve, reject) => {
if (object.type === "rect") {
let source = object.fill.source;
resolve(source);
} else if (object.type === "image") {
// resolve(object.getElement());
// fabric.Image.fromURL(
// object.src,
// v => resolve(v),
// { crossOrigin: "anonymous" }
// );
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_ = {
source: FillSourceToBase64(img),
gapX: 0,
gapY: 0,
width: img.width,
height: img.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 {
rect.set({
width: bgObject.width,
height: bgObject.height,
top: bgObject.top,
left: bgObject.left,
originX: bgObject.originX,
originY: bgObject.originY,
});
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;
}
}

View File

@@ -10,7 +10,6 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
import { ToolCommand } from "./ToolCommands";
import {
findObjectById,
findObjectByLayerId,
generateId,
getObjectZIndex,
insertObjectAtZIndex,
@@ -20,7 +19,7 @@ import {
} from "../utils/helper";
import { fabric } from "fabric-with-all";
import { restoreFabricObject } from "../utils/objectHelper";
import EventManager from "../utils/event.js";
/**
* 添加图层命令
*/
@@ -37,7 +36,7 @@ export class AddLayerCommand extends Command {
this.insertIndex = options.insertIndex;
this.oldActiveLayerId = null;
this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
this.options = options.options || {};
}
@@ -71,7 +70,7 @@ export class AddLayerCommand extends Command {
undo() {
// 从图层列表删除该图层
this.layers.value = JSON.parse(this.beforeLayers);
this.layers.value = [...this.beforeLayers];
// 恢复原活动图层
this.activeLayerId.value = this.oldActiveLayerId;
@@ -252,12 +251,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) => {
@@ -524,7 +523,6 @@ 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(
@@ -601,9 +599,7 @@ 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) {
// 查找最近的非背景层作为新的活动图层
@@ -636,9 +632,6 @@ 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);
// 使用优化渲染批处理恢复真实对象到画布
@@ -656,6 +649,7 @@ export class RemoveLayerCommand extends Command {
}
});
});
await this.layerManager?.updateLayersObjectsInteractivity?.();
this.canvas.renderAll();
@@ -808,23 +802,15 @@ 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) {
this.canvas.getObjects().forEach((obj) => {
if (ids.includes(obj.layerId)) {
obj.getObjects?.()?.forEach((item) => {
item.visible = this.layer.visible;
});
obj.visible = this.layer.visible;
}
});
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.layer.visible;
});
}
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity();
@@ -882,14 +868,13 @@ export class ToggleChildLayerVisibilityCommand extends Command {
// 更新画布上图层对象的可见性
if (this.canvas) {
this.canvas.getObjects().forEach((obj) => {
if (obj.layerId === this.layerId) {
obj.getObjects?.()?.forEach((item) => {
item.visible = this.childLayer.visible;
});
obj.visible = this.childLayer.visible;
}
});
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.childLayer.visible;
});
}
// 更新画布上对象的可选择状态
@@ -1022,8 +1007,9 @@ export class LayerLockCommand extends Command {
// 如果是组图层,递归更新所有子图层
if (
layer.type === "group" &&
layer.children &&
Array.isArray(layer.children) && layer.children.length > 0
Array.isArray(layer.children)
) {
layer.children.forEach((child) => {
this._updateLayerLockState(child, locked);
@@ -1122,7 +1108,7 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
return true;
}
@@ -1144,7 +1130,6 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
}
EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
}
getInfo() {
@@ -1386,7 +1371,7 @@ export class GroupLayersCommand extends Command {
// 备份原图层
this.originalLayers = [...this.layers.value];
// 新组ID
this.groupId = options.id ||
this.groupId =
generateId("group_layer_") ||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
@@ -4291,28 +4276,24 @@ export class RemoveChildLayerCommand extends Command {
}
// 恢复子图层到原位置
this.parentLayer.children.splice(this.childIndex, 0, this.removedChild);
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(); // 更新坐标
});
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);
resolve(true);
});
// 重新渲染画布
await this.layerManager?.updateLayersObjectsInteractivity(false);
});
return true;
}
getInfo() {
@@ -4453,90 +4434,3 @@ 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;
}
}

View File

@@ -1,50 +0,0 @@
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();
}
}

View File

@@ -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;
// 标记为脏对象

View File

@@ -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 = JSON.parse(this.originalLayerStructure);
this.layers.value = [...this.originalLayerStructure];
// 恢复原活动图层
this.activeLayerId.value = this.layerId;
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
*/
_saveOriginalLayerStructure() {
// 只保存相关的图层结构,而不是整个图层数组
this.originalLayerStructure = JSON.stringify(this.layers.value);
this.originalLayerStructure = JSON.parse(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();
}

View File

@@ -2,7 +2,6 @@ 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";
/**
* 对象变换命令
@@ -76,7 +75,7 @@ export class TransformCommand extends Command {
// 触发画布更新
this.canvas.renderAll();
EventManager.emit("object:modified:execute", targetObject);
return true;
}
@@ -114,7 +113,7 @@ export class TransformCommand extends Command {
}, 300);
// 触发画布更新
this.canvas.renderAll();
EventManager.emit("object:modified:undo", targetObject);
return true;
}
@@ -168,7 +167,7 @@ export class TransformCommand extends Command {
);
if (clippingMaskFabricObject) {
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});

View File

@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
return;
}
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});

View File

@@ -1,4 +1,5 @@
<template>
<!-- 图片列表面板 -->
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
<div class="crop-image-modal">
<div class="modal-header">
@@ -391,7 +392,7 @@
<style scoped lang="less">
/* 弹窗遮罩层 */
.crop-image-overlay {
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
@@ -419,8 +420,8 @@
.crop-image-modal {
background-color: #fff;
border-radius: 12px;
width: 90%;
height: 90%;
width: 80%;
height: 80%;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@@ -2,9 +2,7 @@
import { ref, nextTick, computed, inject } from "vue";
import { Checkbox } from "ant-design-vue";
import { VueDraggable } from "vue-draggable-plus";
import { isGroupLayer, SpecialLayerId } from "../../utils/layerHelper";
import { fillToCssStyle, palletToFill, fillToPallet } from "../../utils/helper";
import { SetColorLayerFillCommand } from "../../commands/LayerCommands";
import { isGroupLayer } from "../../utils/layerHelper";
import { useI18n } from 'vue-i18n'
const {t} = useI18n()
// 设置组件名称,用于递归渲染
@@ -185,9 +183,6 @@ function handleToggleVisibility() {
}
function handleToggleLock() {
// 禁用解锁的图层不能操作
if (props.layer.isDisableUnlock) return;
if (props.isChild) {
// 子图层需要传递父图层ID - 从父级组件获取
const parentId = props.layer.parentId || findParentLayerId();
@@ -353,29 +348,6 @@ 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>
@@ -405,8 +377,8 @@ const clickColor = () => {
@contextmenu.prevent="handleContextMenu"
>
<!-- 拖拽手柄 -->
<div class="layer-drag-handle" :title="$t('拖拽排序')" v-if="!isHidenDragHandle">
<SvgIcon :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
<div class="layer-drag-handle" :title="$t('拖拽排序')">
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
</div>
<!-- 图层头部 -->
@@ -445,18 +417,9 @@ const clickColor = () => {
/>
</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" >
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
<!-- 可见性切换 -->
<div
class="visibility-btn"
@@ -471,7 +434,7 @@ const clickColor = () => {
<span
v-if="layer.locked"
class="status-icon locked"
:class="{ disabled: layer.isBackground || layer.isFixed || layer.isDisableUnlock || layer.isFixedOther }"
:class="{ disabled: layer.isBackground || layer.isFixed }"
:title="$t('锁定')"
@click.stop="handleToggleLock"
>

View File

@@ -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 && !layer.isFixedOther);
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
});
// 计算属性:不可排序的固定图层(背景层和固定层)
const fixedLayers = computed(() => {
if (!layers) return [];
return layers.value.filter((layer) => {
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground || layer.isFixedOther);
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
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 {
if(!layer.isFixedClipMask) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
// 普通点击:进入单选模式
// selectedLayerIds.value = [layer.id];
// isMultiSelectMode.value = false;
@@ -596,7 +596,7 @@ function handleLayerClick(layer, event) {
layerManager?.updateLayersObjectsInteractivity();
}
}
if(!layer.isFixedClipMask) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
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 || childLayer.isDisableUnlock,
disabled: childLayer.isBackground || childLayer.isFixed,
action: () => toggleChildLayerLock(childLayer.id),
},
// 显示/隐藏
@@ -1633,6 +1633,7 @@ async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex
@delete-child="deleteChildLayer"
@rename-child="renameChildLayer"
/>
<!-- 固定层背景层和固定层 -->
<div v-if="fixedLayers.length > 0" class="fixed-layers">
<!-- 遍历固定层 -->

View File

@@ -340,14 +340,6 @@
}
}
.layer-color-btn{
width: 30px;
height: 20px;
margin-right: 5px;
border-radius: 2px;
border: 1px solid #000;
}
// 图层操作
.layer-actions {
display: flex;

View File

@@ -384,7 +384,7 @@ async function prepareForLiquify(targetObj) {
}
updateAllParams();
console.log("液化环境准备完成",compositeCommand);
console.log("液化环境准备完成");
}
} catch (error) {
console.error("准备液化环境失败:", error);
@@ -1614,7 +1614,6 @@ function close() {
*/
function startPressTimer() {
if (pressTimer.value) return;
if (currentMode.value === compositeCommand.value.liquifyManager.enhancedManager.modes.PUSH) return;
pressTimer.value = setInterval(() => {
// 计算按压持续时间

View File

@@ -1,199 +0,0 @@
<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">&times;</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>

View File

@@ -1,666 +0,0 @@
<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>

View File

@@ -0,0 +1,744 @@
<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>

View File

@@ -1,133 +0,0 @@
<template>
<div class="repeat-setting">
<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)"
/>
</div>
<p></p>
<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>
<p></p>
<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>
<p></p>
<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>
<p></p>
<div class="repeat-setting-item">
<span class="label">{{ t("Canvas.offset") }}</span>
<offset-tool
:top="(props.object.fill?.offsetY / props.object.height) * 100"
:left="(props.object.fill?.offsetX / props.object.width) * 100"
@input="(e) => emit('inputFillOffset', e)"
@change="(e) => emit('changeFillOffset', e)"
/>
</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 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;
> .repeat-setting-item {
display: flex;
align-items: center;
//虚线
> .label {
min-width: 50px;
font-size: 14px;
}
> .angle-tool {
width: 120px;
}
}
> p {
margin: 10px 0;
width: 100%;
height: 0;
border-bottom: 1px dashed #e5e5e5;
}
}
</style>

View File

@@ -1,47 +0,0 @@
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:"变暗:重叠部分颜色减淡" },
]);

View File

@@ -1,900 +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
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
:title="t('Canvas.repeatSetting')"
>
<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">
<i class="iconfont icon-gengduo"></i>
</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>

View File

@@ -412,12 +412,8 @@ const handleToolClick = (tool) => {
overflow-y: auto;
overflow-x: hidden;
}
.tools-list::-webkit-scrollbar {
display: none;
}
.red-green-mode {
background-color: #060505;
background-color: #fff4f4;
}
.mode-indicator {

View File

@@ -270,13 +270,6 @@
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;

View File

@@ -1,121 +0,0 @@
<template>
<div class="angle-tool">
<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" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch } from "vue";
import { calculateAngle } from "../../utils/helper";
// Props
const props = defineProps({
angle: {
type: Number,
default: 0,
},
});
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) => {
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 = () => emit("input", angle.value);
var changeTime: any = null;
const onChange = () => {
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%;
> .dish {
width: 24px;
height: 24px;
border: 1px solid #000;
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: #000;
border-radius: 50%;
}
}
}
> .input {
margin-left: 5px;
font-size: 14px;
color: #000;
flex: 1;
// min-width: 45px;
// max-width: 80px;
// width: 50px;
> input {
width: 100%;
border-radius: 3px;
outline: none;
}
}
}
</style>

View File

@@ -1,66 +0,0 @@
<template>
<a-select
class="my-select"
:size="size"
@change="change"
:defaultValue="defaultValue"
@dropdownVisibleChange="dropdownVisibleChange"
>
<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({
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>

View File

@@ -1,190 +0,0 @@
<template>
<div class="offset-tool">
<div
class="dish"
@mousedown="mousedown"
@touchstart="mousedown"
ref="dishRef"
>
<span
:style="{ top: data.top + '%', left: data.left + '%' }"
></span>
</div>
<input
class="top"
type="range"
:min="0"
:max="100"
:step="0.1"
v-model="data.top"
@input="onInput"
@change="onChange"
/>
<input
class="left"
type="range"
:min="0"
:max="100"
:step="0.1"
v-model="data.left"
@input="onInput"
@change="onChange"
/>
<span class="tip"
>x:{{ tofix(data.left) }}% y:{{ tofix(data.top) }}%</span
>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch } from "vue";
const props = defineProps({
top: {
type: Number,
default: 50,
},
left: {
type: Number,
default: 50,
},
});
const tofix = (v: number | string) => Number(Number(v).toFixed(1));
const emit = defineEmits(["change", "input"]);
const data = reactive({
top: tofix(props.top),
left: tofix(props.left),
});
watch(
() => props.top,
(v) => (data.top = tofix(v))
);
watch(
() => props.left,
(v) => (data.left = tofix(v))
);
const dishRef = ref<HTMLDivElement>();
const mousedown = (e: MouseEvent | TouchEvent) => {
if (!dishRef.value) return;
const mousemove = (e: MouseEvent | TouchEvent) => {
if (!dishRef.value) return;
const { left, top, width, height } =
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 - left) / width) * 100;
var y = ((Y - top) / height) * 100;
if (x < 0) x = 0;
if (x > 100) x = 100;
if (y < 0) y = 0;
if (y > 100) y = 100;
data.left = tofix(x);
data.top = tofix(y);
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", { ...data });
var changeTime: any = null;
const onChange = () => {
clearTimeout(changeTime);
changeTime = setTimeout(() => emit("change", { ...data }), 500);
};
// var offsetTime = null;
// watch(data, (v) => {
// const obj = { ...v };
// emit("input", obj);
// clearTimeout(offsetTime);
// offsetTime = setTimeout(() => emit("change", obj), 50);
// });
// defineExpose({
// open,
// close,
// });
</script>
<style scoped lang="less">
.offset-tool {
width: 125px;
height: 125px;
display: flex;
position: relative;
overflow: hidden;
--gap: 15px;
> .dish {
margin: var(--gap) 0 0 var(--gap);
flex: 1;
border: 1px solid #000;
border-radius: 5px;
cursor: pointer;
position: relative;
background-color: #fff;
> span {
pointer-events: none;
user-select: none;
position: absolute;
top: 0%;
left: 0%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background-color: #000;
border-radius: 50%;
}
}
> .tip {
position: absolute;
right: 4px;
bottom: 0;
font-size: 10px;
pointer-events: none;
user-select: none;
color: #666;
}
> input.left {
right: 0;
}
> input.top {
bottom: 0;
left: 0;
transform-origin: left bottom;
transform: rotate(90deg) translateX(-100%);
}
> input {
position: absolute;
width: calc(100% - var(--gap));
-webkit-appearance: none;
appearance: none;
height: 8px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
// outline: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: #4285f4; /* 蓝色滑块 */
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
&::-webkit-slider-thumb:hover {
background: #3b77db;
transform: scale(1.1);
}
}
}
</style>

View File

@@ -1,160 +0,0 @@
<template>
<div class="slider">
<div class="input-range">
<span
class="tip"
:style="{
'--progress': (value - props.min) / (props.max - props.min),
}"
>{{ props.tipFormatter(value) }}</span
>
<input
type="range"
v-model="value"
:min="props.min"
:max="props.max"
:step="props.step"
@input="onInput"
@change="onChange"
/>
</div>
<div class="input" v-show="isInput">
<input
type="number"
v-model="value"
:min="props.min"
:max="props.max"
:step="props.step"
@input="onInput"
@change="onChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch } from "vue";
const props = defineProps({
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 = () => emit("input", Number(value.value));
var changeTime: any = null;
const onChange = () => {
clearTimeout(changeTime);
changeTime = setTimeout(() => emit("change", Number(value.value)), 500);
};
</script>
<style scoped lang="less">
.slider {
position: relative;
display: flex;
align-items: center;
--input-thumb-size: 12px;
width: 150px;
// &:focus-within,
&: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;
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
outline: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: var(--input-thumb-size);
height: var(--input-thumb-size);
border-radius: 50%;
background: #4285f4; /* 蓝色滑块 */
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
&::-webkit-slider-thumb:hover {
background: #3b77db;
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>

View File

@@ -35,8 +35,7 @@ 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 PalletPanel from "./components/PalletPanel/index.vue";
import SelectMenuPanel from "./components/SelectMenuPanel/index.vue"; // 引入选择工具菜单组件
import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
import { LayerType, OperationType } from "./utils/layerHelper.js";
import { ToolManager } from "./managers/ToolManager.js";
@@ -58,7 +57,6 @@ const emit = defineEmits([
"changeCanvas", // 画布变更事件
"canvasInit", // 画布初始化事件
"trigger-library", // 触发打开Library选择图片事件
"before-unmount-export-extra-info", // 组件卸载前导出额外信息事件
]);
const props = defineProps({
@@ -66,10 +64,6 @@ const props = defineProps({
type: [Object, String],
default: "", // 默认空
},
otherData: {
type: [Object, null],
default: null, // 默认空对象
},
config: {
type: Object,
default: () => CanvasConfig, // 默认配置
@@ -84,11 +78,7 @@ const props = defineProps({
},
clothingImageUrl: {
type: String,
default: "", // 衣服底图URL-线稿
},
clothingImageUrl2: {
type: String,
default: "", // 衣服底图URL-上色
default: "", // 衣服底图URL
},
redGreenImageUrl: {
type: String,
@@ -260,7 +250,6 @@ onMounted(async () => {
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
isFixedErasable: props.isFixedErasable,
props,
});
canvasManager.canvas.activeLayerId = activeLayerId;
canvasManager.activeLayerId = activeLayerId;
@@ -318,7 +307,6 @@ onMounted(async () => {
canvas: canvasManager.canvas,
commandManager,
layerManager,
canvasManager,
toolManager,
isRedGreenMode,
pasteText: (text) => {
@@ -447,12 +435,6 @@ onMounted(async () => {
canvasManager.canvas.width,
canvasManager.canvas.height
);
if(props.otherData && !props.otherData.canvasId) {
await canvasManager?.createOtherLayers(props.otherData);
await layerManager?.layerSort?.rearrangeObjects();
}
}
// // 设置固定图层是否可擦除
@@ -545,15 +527,12 @@ watchEffect(() => {
}
});
onBeforeUnmount(async () => {
onBeforeUnmount(() => {
// if (import.meta.hot) {
// // 热更新
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
// return; // 开发环境下不卸载组件
// }
const extraInfo = await canvasManager.exportExtraInfo();
emit("before-unmount-export-extra-info", extraInfo);
console.log("onBeforeUnmount 组件卸载,清理资源...");
canvasManager?.dispose?.();
commandManager?.dispose?.();
@@ -741,7 +720,42 @@ 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() {
@@ -888,18 +902,13 @@ const changeCanvas = async (command) => {
...command, // 传递完整的命令数据
};
emit("changeCanvas", commandData);
canvasManager.changeCanvas(commandData);
if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) {
if (command.canUndo || command.canRedo) {
setTimeout(async () => {
try {
const imageData = await canvasManager.exportImage({
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
isCropByBg: true,
});
emit("trigger-red-green-mouseup", imageData);
} catch (error) {
}
const imageData = await canvasManager.exportImage({
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
isCropByBg: true,
});
emit("trigger-red-green-mouseup", imageData);
}, 100);
}
};
@@ -909,14 +918,6 @@ 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);
@@ -1013,7 +1014,6 @@ defineExpose({
exportImage: ({
isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层
isContainFixedOther = false, // 是否包含其他固定图层
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小
layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组
@@ -1023,7 +1023,6 @@ defineExpose({
return canvasManager.exportImage({
isContainBg,
isContainFixed,
isContainFixedOther,
isCropByBg,
layerId,
layerIdArray,
@@ -1031,10 +1030,6 @@ defineExpose({
isEnhanceImg,
});
},
// 导出颜色图层
exportColorLayer: () => {
return canvasManager.exportColorLayer();
},
/**
* 移动图层位置
* @param {string} layerId 图层ID
@@ -1053,14 +1048,6 @@ defineExpose({
return result;
},
/**
* 导出所有信息
* @returns {Object} 包含所有图层信息的对象
*/
exportExtraInfo: () => {
return canvasManager.exportExtraInfo();
},
/**
* 拖拽排序图层
* @param {number} oldIndex 原索引
@@ -1258,7 +1245,6 @@ defineExpose({
:commandManager="commandManager"
:selectionManager="selectionManager"
:layerManager="layerManager"
:canvasManager="canvasManager"
:toolManager="toolManager"
:activeTool="activeTool"
/>
@@ -1283,7 +1269,6 @@ defineExpose({
?
</button>
</div>
</div>
<!-- 图层面板组件 -->
@@ -1313,11 +1298,9 @@ defineExpose({
</div>
</transition>
<!-- 裁剪图片组件 -->
<CropImage ref="cropImageRef" />
</div>
<!-- 裁剪图片组件 -->
<CropImage ref="cropImageRef" />
<!-- 颜色选择器组件 -->
<PalletPanel ref="palletPanelRef" />
<!-- <div class="footer-actions">
<button class="share-btn">Share</button>

View File

@@ -9,13 +9,7 @@ import {
isGroupLayer,
OperationType,
OperationTypes,
findLayer,
createLayer,
LayerType,
SpecialLayerId,
BlendMode,
} from "../utils/layerHelper";
import { ObjectMoveCommand } from "../commands/ObjectCommands";
import { AnimationManager } from "./animation/AnimationManager";
import { createCanvas } from "../utils/canvasFactory";
import { CanvasEventManager } from "./events/CanvasEventManager";
@@ -27,13 +21,6 @@ import {
findObjectById,
generateId,
optimizeCanvasRendering,
palletToFill,
fillToCssStyle,
calculateRotatedTopLeftDeg,
calculateCenterPoint,
createPatternTransform,
getTransformScaleAngle,
base64ToCanvas,
} from "../utils/helper";
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
import { isFunction } from "lodash-es";
@@ -43,11 +30,6 @@ import {
validateLayerAssociations,
} from "../utils/layerUtils";
import { imageModeHandler } from "../utils/imageHelper";
import { getObjectAlphaToCanvas } from "../utils/objectHelper";
import { AddLayerCommand, RemoveLayerCommand } from "../commands/LayerCommands";
import { fa, id } from "element-plus/es/locales.mjs";
import i18n from "@/lang/index.ts";
const {t} = i18n.global;
export class CanvasManager {
constructor(canvasElement, options) {
@@ -68,7 +50,6 @@ export class CanvasManager {
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
this.eraserStateManager = null; // 橡皮擦状态管理器引用
this.handleCanvasInit = null; // 画布初始化回调函数
this.props = options.props || {};
// 初始化画布
this.initializeCanvas();
}
@@ -102,10 +83,10 @@ export class CanvasManager {
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
// 设置画布辅助线
initAligningGuidelines(this.canvas);
// // 设置画布辅助线
// initAligningGuidelines(this.canvas);
// 设置画布中心线
// // 设置画布中心线
// initCenteringGuidelines(this.canvas);
// 初始化画布事件监听器
@@ -450,7 +431,7 @@ export class CanvasManager {
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
* 这样可以保持对象间的相对位置关系不变
*/
async centerAllObjects() {
centerAllObjects() {
if (!this.canvas) return;
// 获取所有可见对象(不是背景元素的对象)
@@ -467,8 +448,8 @@ export class CanvasManager {
// 获取背景对象
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
// !this.canvas?.clipPath &&
// this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
!this.canvas?.clipPath &&
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
this.canvas?.clipPath?.set?.({
left: this.width / 2,
@@ -515,6 +496,7 @@ export class CanvasManager {
// 计算背景层的偏移量
const deltaX = backgroundObject.left - backgroundOldLeft;
const deltaY = backgroundObject.top - backgroundOldTop;
// 将相同的偏移量应用到所有其他对象上
const otherObjects = visibleObjects.filter(
(obj) => obj !== backgroundObject
@@ -567,20 +549,8 @@ export class CanvasManager {
this.updateMaskPosition(backgroundObject);
}
// 更新颜色层信息
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
if(colorObject){
await this.setObjecCliptInfo(colorObject);
}
const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(groupLayer){
const groupRect = new fabric.Rect({});
await this.setObjecCliptInfo(groupRect);
groupLayer.clippingMask = groupRect.toObject();
}
// 重新渲染画布
this.canvas.renderAll();
// this.canvas.renderAll();
}
/**
@@ -630,7 +600,7 @@ export class CanvasManager {
* @param {Number} canvasWidth 画布宽度
* @param {Number} canvasHeight 画布高度
*/
async centerBackgroundLayer(canvasWidth, canvasHeight) {
centerBackgroundLayer(canvasWidth, canvasHeight) {
const backgroundLayerObject = this.getBackgroundLayer();
if (!backgroundLayerObject) return false;
@@ -676,11 +646,6 @@ export class CanvasManager {
if (this.maskLayer) {
this.canvas.remove(this.maskLayer);
}
this.canvas.getObjects().forEach((obj) => {
if (obj.id === "canvasMaskLayer") {
this.canvas.remove(obj);
}
})
// 创建蒙层 - 使用透明矩形作为裁剪区域
this.maskLayer = new fabric.Rect({
@@ -741,82 +706,6 @@ export class CanvasManager {
return backgroundLayerByBgLayer;
}
getFixedLayerObject() {
if (!this.canvas) return null;
const fixedLayer = this.canvas.getObjects().find((obj) => {
return obj.isFixed;
});
if (fixedLayer) return fixedLayer;
// 如果没有找到固定层则根据图层ID查找
const fixedLayerId = this.layers.value.find((layer) => {
return layer.isFixed;
})?.id;
const fixedLayerByFixedLayer = this.canvas.getObjects().find((obj) => {
return obj.isFixed || obj.id === fixedLayerId;
});
if (!fixedLayerByFixedLayer) {
console.warn(
"CanvasManager.js = >getFixedLayerObject 方法没有找到固定层"
);
}
return fixedLayerByFixedLayer;
}
getBackgroundLayerObject() {
if (!this.canvas) return null;
const backgroundLayer = this.canvas.getObjects().find((obj) => {
return obj.isBackground;
});
if (backgroundLayer) return backgroundLayer;
// 如果没有找到背景层则根据图层ID查找
const backgroundLayerId = this.layers.value.find((layer) => {
return layer.isBackground;
})?.id;
const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => {
return obj.isBackground || obj.id === backgroundLayerId;
});
if (!backgroundLayerByBgLayer) {
console.warn(
"CanvasManager.js = >getBackgroundLayerObject 方法没有找到背景层"
);
}
return backgroundLayerByBgLayer;
}
getLayerObjectById(layerId) {
if (!this.canvas) return null;
const layerObject = this.canvas.getObjects().find((obj) => {
return obj.id === layerId;
});
if (layerObject) return layerObject;
// 如果没有找到图层对象则根据图层ID查找
const layerObjectByLayerId = this.canvas.getObjects().find((obj) => {
return obj.id === layerId;
});
if (!layerObjectByLayerId) {
console.warn(
"CanvasManager.js = >getLayerObjectById 方法没有找到图层对象"
);
}
return layerObjectByLayerId;
}
getObjectsByIds(ids){
const objects = this.canvas.getObjects().filter((obj) => {
return ids.includes(obj.id);
});
return objects;
}
/**
* 更新蒙层位置
* @param {Object} backgroundLayerObject 背景层对象
@@ -909,7 +798,7 @@ export class CanvasManager {
// 如果找到了图层,则生成缩略图
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
this.layerManager?.sortLayers?.();
return result;
}
@@ -918,13 +807,11 @@ export class CanvasManager {
* @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
* @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @returns {String} 导出的图片数据URL
*/
async exportImage(options = {}) {
@@ -945,7 +832,6 @@ export class CanvasManager {
options.restoreOpacityInRedGreen !== undefined
? options.restoreOpacityInRedGreen
: false, // 默认在红绿图模式下恢复透明度
excludedLayers: [SpecialLayerId.SPECIAL_GROUP],
};
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
@@ -960,7 +846,7 @@ export class CanvasManager {
const normalLayerIds =
this.layers?.value
?.filter(
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther && layer.visible
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
)
?.map((layer) => layer.id) || [];
@@ -971,141 +857,11 @@ export class CanvasManager {
}
return await this.exportManager.exportImage(enhancedOptions);
} catch (error) {
console.warn("CanvasManager导出图片失败:", error);
console.error("CanvasManager导出图片失败:", error);
throw error;
}
}
/**
* 导出印花元素颜色信息
* @returns {Object}
*/
async exportExtraInfo() {
// 导出颜色图层信息
const color = await this.exportColorLayer().catch(() => (null));
// 导出印花和元素图层信息
const printTrimsData = await this.exportPrintTrimsLayers().catch(() => ({prints: null, trims: null}));
return {
color,
...printTrimsData,
};
}
/**
* 导出颜色图层
* @returns {Object} 导出的颜色图层数据URL
*/
async exportColorLayer() {
if (!this.exportManager) {
console.warn("导出管理器未初始化,请确保已设置图层管理器");
return Promise.reject("颜色图层不存在");
}
const object = this.getLayerObjectById(SpecialLayerId.COLOR);
if(!object){
console.warn("颜色图层不存在,请确保已添加颜色图层");
return Promise.reject("颜色图层不存在");
}
const css = fillToCssStyle(object.fill)
const canvas = new fabric.StaticCanvas();
canvas.setDimensions({
width: object.width,
height: object.height,
backgroundColor: null,
imageSmoothingEnabled: true,
});
const cloneObject = await new Promise((resolve, reject) => {
object.clone(resolve);
});
cloneObject.set({
left: canvas.width / 2,
top: canvas.height / 2,
scaleX: 1,
scaleY: 1,
visible: true,
clipPath: null,
});
canvas.add(cloneObject);
canvas.renderAll();
const base64 = canvas.toDataURL({
format: "png",
quality: 1,
});
canvas.clear();
const color = object.originColor;
return {css, base64, color};
}
/**
* 导出印花和元素图层
*/
async exportPrintTrimsLayers() {
const object = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(!object) return Promise.reject("印花和元素图层组不存在");
const ids = object.children.map((v) => v.id);
const objects = this.getObjectsByIds(ids).filter((v) => !!v.sourceData);
const fixedLayerObj = this.getFixedLayerObject();
if(!fixedLayerObj) return Promise.reject("固定图层不存在");
const flWidth = fixedLayerObj.width
const flHeight = fixedLayerObj.height
const flTop = fixedLayerObj.top
const flLeft = fixedLayerObj.left
const flScaleX = fixedLayerObj.scaleX
const flScaleY = fixedLayerObj.scaleY
const prints = [];
const trims = [];
objects.forEach((v) => {
const obj = {
ifSingle: v.sourceData.ifSingle,
level2Type: v.sourceData.level2Type,
designType: v.sourceData.designType,
path: v.sourceData.path,
minIOPath: v.sourceData.minIOPath,
location: [0, 0],
scale: [0, 0],
angle: v.angle,
name: v.sourceData.name,
priority: v.sourceData.priority,
}
if(obj.ifSingle){
let left = (v.left - (flLeft - flWidth * flScaleX / 2));
let top = (v.top - (flTop - flHeight * flScaleY / 2));
let width = (v.width * v.scaleX);
let height = (v.height * v.scaleY);
let {x:cx, y:cy} = calculateCenterPoint(width, height, left, top, v.angle);
let x = (cx-width/2) / flScaleX;
let y = (cy-height/2) / flScaleY;
obj.location = [x, y];
obj.scale = [(v.width * v.scaleX) / (flWidth * flScaleX), (v.height * v.scaleY) / (flHeight * flScaleY)];
}else{
let fill = v.fill;
let fill_ = v.fill_;
if(!fill || !fill_) return;
let {scale, angle} = getTransformScaleAngle(fill.patternTransform);
let scaleX = scale * 5 * v.fill_.width / flWidth;
let scaleY = scale * 5 * v.fill_.height / flHeight;
let scaleXY = flWidth > flHeight ? scaleX : scaleY;
let left = fill.offsetX + v.fill_.width * scale / 2;
let top = fill.offsetY + v.fill_.height * scale / 2;
obj.scale = [scaleXY, scaleXY];
obj.angle = angle;
obj.location = [left, top];
}
if(obj.level2Type === "Pattern"){
prints.push(obj);
}else if(obj.level2Type === "Embroidery"){
trims.push(obj);
}
})
// prints.sort((a, b) => a.ifSingle ? 1 : -1);
prints.forEach((v, i) => v.priority = i + 1);
trims.forEach((v, i) => v.priority = i + 1);
return {prints, trims};
}
dispose() {
// 释放导出管理器资源
if (this.exportManager) {
@@ -1200,58 +956,38 @@ export class CanvasManager {
// };
try {
// 清除画布中选中状态
// this.canvas.discardActiveObject();
this.canvas.discardActiveObject();
this.canvas.renderAll();
// 排除颜色图层和特殊组图层
const excludedLayers = [SpecialLayerId.COLOR, SpecialLayerId.SPECIAL_GROUP];
this.layers.value.forEach((layer) => {
if(excludedLayers.includes(layer.id)){
excludedLayers.push(...layer.children?.map((child) => child.id));
}
})
const canvas = this.canvas.toJSON([
"id",
"type",
"layerId",
"layerName",
"isBackground",
"isLocked",
"isVisible",
"isFixed",
"parentId",
"eraser",
"eraserable",
"erasable",
"customType",
"fill_",
"scaleX",
"scaleY",
"top",
"left",
"width",
"height",
]);
canvas.objects = canvas.objects.filter((v) => !excludedLayers.includes(v.layerId));
const simplifyLayersData = simplifyLayers(
JSON.parse(JSON.stringify(this.layers.value)),
excludedLayers
JSON.parse(JSON.stringify(this.layers.value))
);
const data = {
canvas,
console.log("获取画布JSON数据...", simplifyLayersData);
return JSON.stringify({
canvas: this.canvas.toJSON([
"id",
"type",
"layerId",
"layerName",
"isBackground",
"isLocked",
"isVisible",
"isFixed",
"parentId",
"eraser",
"eraserable",
"erasable",
"customType",
]),
layers: simplifyLayersData, // 简化图层数据
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
version: "1.0", // 添加版本信息
timestamp: new Date().toISOString(), // 添加时间戳
canvasWidth: this.canvasWidth.value,
canvasHeight: this.canvasHeight.value,
canvasColor: this.canvasColor.value,
activeLayerId: this.layerManager?.activeLayerId?.value,
};
console.log("获取画布JSON数据...", data);
return JSON.stringify(data);
});
} catch (error) {
console.error("获取画布JSON失败:", error);
throw new Error("获取画布JSON失败");
@@ -1334,10 +1070,8 @@ export class CanvasManager {
// }
try {
// 重置画布数据
await this.setCanvasSize(this.canvas.width, this.canvas.height);
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
await this.createOtherLayers(this.props.otherData);
this.setCanvasSize(this.canvas.width, this.canvas.height);
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
// 重新构建对象关系
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
// 验证图层关联关系 - 稳定后可以注释
@@ -1365,7 +1099,9 @@ export class CanvasManager {
await calllBack?.();
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.();
await this.layerManager?.updateLayersObjectsInteractivity?.(
false
);
console.log(this.layerManager.layers.value);
// 更新所有缩略图
@@ -1390,314 +1126,6 @@ export class CanvasManager {
}
}
/**
* 创建其他图层:印花、颜色、元素...
* @param {Object} otherData - 其他图层数据
*/
async createOtherLayers(otherData) {
if (!otherData) return console.warn("otherData 为空不需要添加");
const otherData_ = JSON.parse(JSON.stringify(otherData));
console.log("==========创建其他图层", otherData_);
// 删除颜色图层和特殊组图层
const ids = [SpecialLayerId.COLOR, SpecialLayerId.SPECIAL_GROUP];
this.layers.value = this.layers.value.filter((layer) => {
if(ids.includes(layer.id)){
ids.push(...layer.children?.map((child) => child.id));
return false;
}
return true;
})
this.canvas.getObjects().forEach((v) => ids.includes(v.id) && this.canvas.remove(v))
// 创建颜色图层
await this.createColorLayer(otherData_.color);
const printTrimsLayers = [];// 印花和元素图层
const singleLayers = [];// 平铺图层
otherData_?.printObject?.prints?.forEach((print, index) => {
print.name = t("Canvas.Print") + (index + 1);
if(print.ifSingle){
printTrimsLayers.unshift({...print});
}else{
singleLayers.unshift({...print});
}
})
otherData_?.trims?.prints?.forEach((trims, index) => {
trims.name = t("Canvas.Elements") + (index + 1);
printTrimsLayers.unshift({...trims});
})
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
await this.changeCanvas();
}
// 设置画布对象的裁剪信息
async setObjecCliptInfo(tagObject, data){
const fixedLayerObj = this.getFixedLayerObject();
if(!fixedLayerObj) return console.warn("固定图层为空");
tagObject.set({
top: fixedLayerObj.top,
left: fixedLayerObj.left,
width: fixedLayerObj.width,
height: fixedLayerObj.height,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
scaleX: fixedLayerObj.scaleX,
scaleY: fixedLayerObj.scaleY,
});
var object = fixedLayerObj;
const imageUrl = this.props.clothingImageUrl2;
if(imageUrl){
object = await new Promise((resolve, reject) => {
fabric.Image.fromURL(imageUrl, (imgObject) => {
tagObject.set({
width: imgObject.width,
height: imgObject.height,
});
resolve(imgObject);
}, { crossOrigin: "anonymous" });
});
}
const canvas = getObjectAlphaToCanvas(object, data);
const transparentMask = new fabric.Image(canvas, {
top: 0,
left: 0,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
});
tagObject.set('clipPath', transparentMask);
}
async createColorLayer(color){
if(!color) return console.warn("颜色为空不需要添加");
// if(findLayer(this.layers.value, SpecialLayerId.COLOR)) {
// return console.warn("画布中已存在颜色图层");
// }
console.log("==========添加颜色图层", color, this.layers.value.length)
// 创建颜色图层对象
const colorRect = new fabric.Rect({
id: SpecialLayerId.COLOR,
layerId: SpecialLayerId.COLOR,
layerName: t("Canvas.color"),
isVisible: true,
isLocked: true,
selectable: false,
hasControls: false,
hasBorders: false,
globalCompositeOperation: BlendMode.MULTIPLY,
originColor: color,
});
await this.setObjecCliptInfo(colorRect);
const gradientObj = palletToFill(color);
const gradient = new fabric.Gradient({
type: 'linear',
gradientUnits: 'percentage',
...gradientObj,
})
colorRect.set('fill', gradient);
this.canvas.add(colorRect);
// 创建颜色图层
const colorLayer = createLayer({
id: colorRect.layerId,
name: colorRect.layerName,
type: LayerType.SHAPE,
visible: colorRect.isVisible,
locked: colorRect.isLocked,
opacity: 1.0,
isFixedOther: true,
blendMode: BlendMode.MULTIPLY,
fabricObjects: [colorRect.toObject(["id", "layerId", "layerName"])],
})
const groupIndex = this.layers.value.findIndex(layer => layer.isFixed || layer.isBackground);
this.layers.value.splice(groupIndex, 0, colorLayer);
}
// 创建印花和元素图层
async createPrintTrimsLayers(printTrimsLayers, singleLayers){
// if(findLayer(this.layers.value, SpecialLayerId.SPECIAL_GROUP)) {
// return console.warn("画布中已存在印花和元素组图层");
// }
console.log("==========添加印花和元素图层组", printTrimsLayers, singleLayers)
const fixedLayerObj = this.getFixedLayerObject();
const flWidth = fixedLayerObj.width
const flHeight = fixedLayerObj.height
const flTop = fixedLayerObj.top
const flLeft = fixedLayerObj.left
const flScaleX = fixedLayerObj.scaleX
const flScaleY = fixedLayerObj.scaleY
const children = [];
// 添加印花和元素图层
for(let index = 0; index < printTrimsLayers.length; index++){
let item = printTrimsLayers[index];
let id = generateId("layer_image_");
let name = item.name;
let image = await new Promise(resolve => {
fabric.Image.fromURL(item.path, (fabricImage)=>{
const left = flLeft - flWidth * flScaleX / 2 + (item.location?.[0] || 0) * flScaleX
const top = flTop - flHeight * flScaleY / 2 + (item.location?.[1] || 0) * flScaleY
const scaleX = flWidth * (item.scale?.[0] || 1) / fabricImage.width * flScaleX
const scaleY = flHeight * (item.scale?.[1] || 1) / fabricImage.height * flScaleY
const {x, y} = calculateRotatedTopLeftDeg(
fabricImage.width * scaleX,
fabricImage.height * scaleY,
left,
top,
0,
item.angle || 0
)
const angle = item.angle || 0
fabricImage.set({
left: x,
top: y,
scaleX: scaleX,
scaleY: scaleY,
angle: angle,
id: id,
layerId: id,
layerName: name,
selectable: true,
hasControls: true,
hasBorders: true,
sourceData: item,
});
resolve(fabricImage);
}, { crossOrigin: "anonymous" });
})
this.canvas.add(image);
let layer = createLayer({
id: id,
name: name,
type: LayerType.BITMAP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
})
children.push(layer);
};
// 添加平铺图层
for(let index = 0; index < singleLayers.length; index++){
let item = singleLayers[index];
let id = generateId("layer_image_");
let name = item.name;
let image = await new Promise(resolve => {
fabric.Image.fromURL(item.path, (fabricImage)=>{
const imgElement = fabricImage.getElement();
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);
}, { crossOrigin: "anonymous" });
})
let scaleX = fixedLayerObj.width / image.width * (item.scale?.[0] || 1) / 5;
let scaleY = fixedLayerObj.height / image.height * (item.scale?.[1] || 1) / 5;
let scale = fixedLayerObj.width > fixedLayerObj.height ? scaleX : scaleY;
let left = (item.location?.[0] || 0) - image.width * scale / 2
let top = (item.location?.[1] || 0) - image.height * scale / 2
let rect = new fabric.Rect({
id: id,
layerId: id,
layerName: name,
width: fixedLayerObj.width,
height: fixedLayerObj.height,
top: fixedLayerObj.top,
left: fixedLayerObj.left,
scaleX: fixedLayerObj.scaleX,
scaleY: fixedLayerObj.scaleY,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
sourceData: item,
fill: new fabric.Pattern({
source: image,
repeat: "repeat",
patternTransform: createPatternTransform(scale, item.angle || 0),
offsetX: left, // 水平偏移
offsetY: top, // 垂直偏移
}),
fill_ : {
source: item.path,
gapX: 0,
gapY: 0,
width: image.width,
height: image.height,
}
});
this.canvas.add(rect);
let layer = createLayer({
id: id,
name: name,
type: LayerType.BITMAP,
visible: true,
locked: true,
opacity: 1,
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
})
children.push(layer);
};
if(children.length === 0){
let layer = createLayer({
id: generateId("layer_image_"),
name: t("Canvas.EmptyLayer"),
type: LayerType.BITMAP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
})
children.push(layer);
}
const groupRect = new fabric.Rect({});
await this.setObjecCliptInfo(groupRect);
// 插入组图层
const groupIndex = this.layers.value.findIndex(layer => layer.isFixedOther || layer.isFixed || layer.isBackground);
const groupLayer = createLayer({
id: SpecialLayerId.SPECIAL_GROUP,
name: t("Canvas.PrintAndElementsGroup"),
type: LayerType.GROUP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
children: children,
clippingMask: groupRect.toObject(),
isFixedClipMask: true,
});
this.layers.value.splice(groupIndex, 0, groupLayer);
}
/**
* 画布事件变更后
*/
async changeCanvas(){
// const fixedLayerObj = this.getFixedLayerObject();
// if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj)
// const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
// if(colorObject){
// const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP);
// if(ids.length === 0){
// ids.unshift(SpecialLayerId.SPECIAL_GROUP);
// await this.setObjecCliptInfo(colorObject);
// this.canvas.renderAll();
// return;
// }
// const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true});
// if(!base64) return console.warn("导出图片失败", base64)
// const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true);
// const ctx = canvas.getContext('2d');
// const width = fixedLayerObj.width;
// const height = fixedLayerObj.height;
// const x = (canvas.width - width) / 2;
// const y = (canvas.height - height) / 2;
// const data = ctx.getImageData(x, y, width, height);
// await this.setObjecCliptInfo(colorObject, data);
// this.canvas.renderAll();
// }
}
/**
* 缩放红绿图模式内容以适应当前画布大小
* 确保衣服底图和红绿图永远在画布内可见
@@ -1821,7 +1249,6 @@ export class CanvasManager {
return fixedLayer.fabricObject || null;
}
/**
* 获取所有普通图层对象(包括红绿图)
* @returns {Array} 普通图层对象数组
@@ -1888,46 +1315,4 @@ export class CanvasManager {
return sizeMatch && positionMatch;
}
/**
* 键盘移动激活对象
* @param {String} direction 移动方向up, down, left, right
* @param {<Number>} step 移动步长
* @private
*/
moveActiveObject(direction, step = 1) {
const objects = [];
const activeObject = this.canvas.getActiveObject();
if(!activeObject) return;
const initPos = {
id: activeObject.id,
left: activeObject.left,
top: activeObject.top,
};
switch(direction) {
case "up":
activeObject.top -= step;
break;
case "down":
activeObject.top += step;
break;
case "left":
activeObject.left -= step;
break;
case "right":
activeObject.left += step;
break;
}
if(!activeObject.id) return this.canvas.renderAll();
const cmd = new ObjectMoveCommand({
canvas: this.canvas,
initPos,
finalPos: {
id: activeObject.id,
left: activeObject.left,
top: activeObject.top,
},
});
this.commandManager.executeCommand(cmd);
}
}

View File

@@ -1,7 +1,6 @@
import { fabric } from "fabric-with-all";
import { findObjectById } from "../utils/helper";
import { createRasterizedImage } from "../utils/selectionToImage";
import { OperationType, SpecialLayerId } from "../utils/layerHelper";
/**
* 图片导出管理器
@@ -19,39 +18,26 @@ export class ExportManager {
* @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @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 {Array} options.excludedLayers 排除的图层ID数组
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @returns {String} 导出的图片数据URL
*/
async exportImage(options = {}) {
exportImage(options = {}) {
const {
isContainBg = false,
isContainFixed = false,
isContainFixedOther = false, // 是否包含其他固定图层
isCropByBg = false, // 是否使用背景大小裁剪
layerId = "",
layerIdArray = [],
layerIdArray2 = null,
expPicType = "png",
restoreOpacityInRedGreen = true,
isEnhanceImg, // 是否是增强图片
excludedLayers = [], // 排除的图层ID数组
isEnhanceImg, // 是否是增强图片
} = options;
try {
// 查找颜色图层并隐藏
// const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR);
// if (colorLayer && colorLayer.visible) {
// colorLayer.visible = false;
// await this.layerManager?.updateLayersObjectsInteractivity();
// }
// 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
// 如果指定了具体图层ID导出指定图层
@@ -62,7 +48,7 @@ export class ExportManager {
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
);
}
@@ -73,11 +59,10 @@ export class ExportManager {
expPicType,
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
);
}
@@ -86,13 +71,10 @@ export class ExportManager {
expPicType,
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
layerIdArray2,
excludedLayers, // 排除的图层ID数组
isEnhanceImg, // 是否是增强图片
);
} catch (error) {
console.error("导出图片失败:", error);
@@ -146,6 +128,8 @@ export class ExportManager {
objectsToExport,
expPicType,
restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
);
}
@@ -165,7 +149,6 @@ 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 是否使用背景大小裁剪
@@ -178,7 +161,6 @@ export class ExportManager {
expPicType,
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
@@ -192,8 +174,7 @@ export class ExportManager {
const objectsToExport = this._collectObjectsByLayerOrder(
layerIdArray,
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
isContainFixed
);
if (objectsToExport.length === 0) {
@@ -225,12 +206,10 @@ 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
*/
@@ -238,21 +217,16 @@ export class ExportManager {
expPicType,
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
layerIdArray, // 导出所有图层
excludedLayers, // 排除的图层ID数组
isEnhanceImg, // 是否是增强图片
) {
// 按图层顺序收集对象(从底到顶)
const objectsToExport = this._collectObjectsByLayerOrder(
layerIdArray, // 导出所有图层
null, // 导出所有图层
isContainBg,
isContainFixed,
isContainFixedOther, // 是否包含其他固定图层
excludedLayers,
);
if (objectsToExport.length === 0) {
@@ -308,11 +282,10 @@ export class ExportManager {
/**
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象
* @param {Object} layer 图层对象
* @param {Boolean} isChildren 是否递归收集子图层的对象
* @returns {Array} 画布中的真实对象数组
* @private
*/
_collectObjectsFromLayer(layer, isChildren = true) {
_collectObjectsFromLayer(layer) {
if (!layer) {
return [];
}
@@ -341,10 +314,10 @@ export class ExportManager {
}
// 递归收集子图层的对象
if (isChildren && layer.children && layer.children.length > 0) {
if (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, isChildren);
const childObjects = this._collectObjectsFromLayer(childLayer);
realObjects.push(...childObjects);
}
}
@@ -410,14 +383,12 @@ 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, isContainFixedOther, excludedLayers) {
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
const objectsToExport = [];
const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
for (let i = allLayers.length - 1; i >= 0; i--) {
@@ -427,11 +398,11 @@ export class ExportManager {
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
// 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther))
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
continue;
if (layer.visible) {
const layerObjects = this._collectObjectsFromLayer(layer, false);
const layerObjects = this._collectObjectsFromLayer(layer);
objectsToExport.push(...layerObjects);
}
}
@@ -440,19 +411,15 @@ export class ExportManager {
}
/**
* 获取扁平化的图层列表(包含子图层),排除指定的图层
* @param {Array} excludedLayers 排除的图层ID数组
* 获取扁平化的图层列表(包含子图层)
* @returns {Array} 扁平化的图层数组
* @private
*/
_getAllLayersFlattened(excludedLayers) {
_getAllLayersFlattened() {
const flattenedLayers = [];
const rootLayers = this._getAllLayers();
const flattenLayer = (layer) => {
// 检查是否在排除列表中
if (excludedLayers && excludedLayers.includes(layer.id)) return;
flattenedLayers.push(layer);
// 递归处理子图层
@@ -467,6 +434,7 @@ export class ExportManager {
for (const layer of rootLayers) {
flattenLayer(layer);
}
return flattenedLayers;
}
@@ -587,22 +555,37 @@ export class ExportManager {
);
}
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
// 使用固定图层的实际显示尺寸作为导出画布尺寸
const canvasWidth = (fixedLayerObject.width);
const canvasHeight = (fixedLayerObject.height);
const canvasWidth = Math.round(fixedBounds.width);
const canvasHeight = Math.round(fixedBounds.height);
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
const tempFabricCanvas = new fabric.StaticCanvas()
tempFabricCanvas.setDimensions({
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, {
width: canvasWidth,
height: canvasHeight,
backgroundColor: null,
// enableRetinaScaling: true,
enableRetinaScaling: true,
imageSmoothingEnabled: true,
});
// tempFabricCanvas.setZoom(1);
console.log("==========", fixedLayerObject)
tempFabricCanvas.setZoom(1);
try {
// 获取裁剪路径对象(如果存在)
const clipPathObject = await this._getClipPathObject(fixedBounds);
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i];
@@ -611,17 +594,20 @@ export class ExportManager {
restoreOpacityInRedGreen && true
);
if (cloned) {
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
cloned.set({
left: canvasWidth / 2,
top: canvasHeight / 2,
scaleX: cloned.scaleX / fixedLayerObject.scaleX,
scaleY: cloned.scaleY / fixedLayerObject.scaleY,
originX: "center",
originY: "center",
left: cloned.left - fixedBounds.left,
top: cloned.top - fixedBounds.top,
});
console.log("==========", {...cloned})
// 更新对象坐标
cloned.setCoords();
// 设置裁剪路径到对象
if (clipPathObject) {
cloned.clipPath = clipPathObject;
}
tempFabricCanvas.add(cloned);
}
}
@@ -630,7 +616,7 @@ export class ExportManager {
tempFabricCanvas.renderAll();
// 生成图片
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
return this._generateHighQualityDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
@@ -750,7 +736,7 @@ export class ExportManager {
*/
_cloneObjectAsync(
obj,
propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"]
propertiesToInclude = ["id", "layerId", "layerName", "name"]
) {
return new Promise((resolve, reject) => {
if (!obj) {
@@ -1045,11 +1031,10 @@ export class ExportManager {
* @param {Object} layer 图层对象
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
* @returns {Boolean} 是否应该包含
* @private
*/
_shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther) {
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
if (!layer) return false;
// 检查背景图层
@@ -1062,11 +1047,6 @@ export class ExportManager {
return isContainFixed;
}
// 检查其他固定图层
if (layer.isFixedOther) {
return isContainFixedOther;
}
// 普通图层总是包含
return true;
}

View File

@@ -31,7 +31,6 @@ import {
} from "../commands/ObjectLayerCommands";
import {
LayerType,
SpecialLayerId,
BlendMode,
createLayer,
createBackgroundLayer,
@@ -344,36 +343,35 @@ export class LayerManager {
});
// 批量更新对象
for(let obj of objects){
let layer = layerMap[obj.layerId];
objects.forEach(async (obj) => {
const layer = layerMap[obj.layerId];
if (!obj.layerId) {
// 没有关联图层的对象使用默认设置
obj.selectable = false;
obj.evented = false;
obj.erasable = false; // 未关联图层的对象不可擦除
break;
return;
}
if (!layer) break;
if (!layer) return;
// 设置一级图层对象的交互性
await this._setObjectInteractivity(obj, layer, editorMode);
// 设置子图层对象的交互性
for(let childLayer of layer.children){
let childObj = this.canvas
layer?.children?.forEach(async (childLayer) => {
const childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
await this._setObjectInteractivity(childObj, childLayer, editorMode);
}
};
};
});
});
// 设置裁剪对象
for(let layer of layers){
if(layer.id === SpecialLayerId.COLOR) break;
layers.forEach(async (layer) => {
let clippingMaskFabricObject = null;
if (layer.clippingMask) {
// 反序列化 clippingMask
@@ -381,7 +379,7 @@ export class LayerManager {
layer.clippingMask,
this.canvas
);
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位
@@ -405,7 +403,7 @@ export class LayerManager {
.find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = clippingMaskFabricObject;
// childObj.dirty = true; // 标记为脏对象
childObj.dirty = true; // 标记为脏对象
childObj.setCoords();
}
@@ -501,7 +499,7 @@ export class LayerManager {
isOldSelectObject
);
}
};
});
}
/**
@@ -524,16 +522,15 @@ 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 = {}, isCmd = true) {
async createLayer(name = null, type = LayerType.EMPTY, options = {}) {
// 生成唯一ID
const layerId = options.id || options.layerId || generateId("layer_");
// 计算普通图层数量(非背景、非固定)
const normalLayersCount = this.layers.value.filter(
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther
(layer) => !layer.isBackground && !layer.isFixed
).length;
// 计算插入位置如果没有指定insertIndex则根据当前选中图层决定插入位置
// 添加到图层列表
@@ -545,7 +542,7 @@ export class LayerManager {
// 创建新图层
const newLayer = createLayer({
id: layerId,
name: name || this.t("Canvas.EmptyLayer"),
name: name || `图层 ${normalLayersCount + 1}`,
type: type,
visible: true,
locked: false,
@@ -574,13 +571,13 @@ export class LayerManager {
}
// 执行命令
if (isCmd && this.commandManager) {
if (this.commandManager) {
await this.commandManager.execute(command);
} else{
} else {
await command.execute();
}
return isCmd ? layerId : command;
return layerId;
}
/**
@@ -955,28 +952,18 @@ export class LayerManager {
// 查找要删除的图层
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
// 如果是背景层或固定层,不允许删除
if (layer && (layer.isBackground || layer.isFixed || layer.isFixedOther)) {
if (layer && (layer.isBackground || layer.isFixed)) {
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
message.warning(layer.isBackground ? this.t("Canvas.backLayerCannotDelete") : this.t("Canvas.fixedLayerCannotDelete"));
message.warning(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
return false;
}
// 检查是否是唯一的普通图层
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);
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
console.log("普通图层:", normalLayers)
if (isChild ? parentLength <= 1 : false) {//normalLayers.length <= 1
if (normalLayers.length === 1) {
console.warn("不能删除唯一的普通图层");
message.warning(this.t("Canvas.cannotDeleteOnlyLayer"));
message.warning("不能删除唯一的普通图层");
return false;
}
// // 如果图层有子图层,提示确认
@@ -1145,7 +1132,7 @@ export class LayerManager {
return acc;
}, []);
console.log("==========", allObjects)
// if (layer.fill) {
// // 如果图层有填充颜色,设置所有对象的填充颜色
// const { object } = findObjectById(this.canvas, layer.fill.id);
@@ -1591,12 +1578,6 @@ 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;
});
@@ -1867,9 +1848,9 @@ export class LayerManager {
}
// 检查是否是唯一的普通图层
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
console.log("普通图层:", normalLayers)
if (normalLayers.length <= 1) {
if (normalLayers.length === 1) {
console.warn("不能剪切唯一的普通图层");
return null;
}
@@ -3269,7 +3250,7 @@ export class LayerManager {
* @private
*/
_setupGroupMaskMovementSync(activeSelection, layer) {
if (!activeSelection || !layer || !layer.clippingMask || layer.isFixedClipMask) {
if (!activeSelection || !layer || !layer.clippingMask) {
return;
}
@@ -3333,6 +3314,7 @@ export class LayerManager {
// 计算移动距离
const deltaX = target.left - initialLeft;
const deltaY = target.top - initialTop;
// 创建更新遮罩位置的命令
const command = new UpdateGroupMaskPositionCommand({
canvas: this.canvas,
@@ -3437,22 +3419,4 @@ 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;
}
}

View File

@@ -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,13 +128,8 @@ export class ThumbnailManager {
}
const { layer } = findLayerRecursively(this.layers.value, layerId);
if (!layer) {
console.warn("⚠️ 无效的图层,无法收集对象");
return [];
}
let layersToRasterize = [];
if (layer.children && layer.children.length > 0) {
// 组图层:收集自身和所有子图层
layersToRasterize = this._collectLayersToRasterize(layer);

View File

@@ -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);
},
});
}

View File

@@ -8,7 +8,6 @@ import { PerformanceManager } from "./PerformanceManager.js";
*/
export class CommandManager {
constructor(options = {}) {
this.canvas = options.canvas;
this.undoStack = [];
this.redoStack = [];
this.maxHistorySize = options.maxHistorySize || 50;
@@ -206,7 +205,6 @@ export class CommandManager {
const startTime = performance.now();
try {
this.canvas?.discardActiveObject();
const command = this.undoStack.pop();
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
@@ -245,7 +243,6 @@ export class CommandManager {
const startTime = performance.now();
try {
this.canvas?.discardActiveObject();
const command = this.redoStack.pop();
console.log(`↪️ 重做命令: ${command.constructor.name}`);

View File

@@ -688,6 +688,7 @@ export class CanvasEventManager {
this.layerManager.commandManager.execute(transformCmd, {
name: "对象修改",
});
// 清除临时状态记录
delete activeObj._initialTransformState;
}

View File

@@ -10,7 +10,6 @@ export class KeyboardManager {
* @param {Object} options.toolManager 工具管理器实例
* @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.layerManager 图层管理器实例
* @param {Object} options.canvasManager 画布管理器实例
* @param {Function} options.pasteText 粘贴文本回调函数
* @param {Function} options.pasteImage 粘贴图片回调函数
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
@@ -20,7 +19,6 @@ export class KeyboardManager {
this.toolManager = options.toolManager;
this.commandManager = options.commandManager;
this.layerManager = options.layerManager;
this.canvasManager = options.canvasManager;
this.container = options.container || document;
this.pasteText = options.pasteText || (() => {});
this.pasteImage = options.pasteImage || (() => {});
@@ -127,10 +125,6 @@ export class KeyboardManager {
// 删除
delete: { action: "delete", description: "删除" },
backspace: { action: "delete", description: "删除" },
up: { action: "up", description: "上" },
down: { action: "down", description: "下" },
left: { action: "left", description: "左" },
right: { action: "right", description: "右" },
// 选择
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
@@ -494,14 +488,6 @@ export class KeyboardManager {
}
break;
case "up":
case "down":
case "left":
case "right":
// 方向键逻辑
this.canvasManager.moveActiveObject(action);
break;
case "increaseBrushSize":
// 增大画笔尺寸
if (this.toolManager && this.toolManager.brushManager) {
@@ -653,6 +639,7 @@ export class KeyboardManager {
if (event.altKey) shortcutKey += "alt+";
const key = event.key.toLowerCase();
// 特殊键处理
switch (key) {
case " ":

View File

@@ -12,7 +12,7 @@ export class LiquifyCPUManager {
sharpenAmount: 0.3, // 添加锐化强度参数
...options,
};
console.log("CPU版本的液化管理器config", this.config);
console.log("CPU版本的液化管理器config",this.config);
this.params = {
size: 60, // 增大默认尺寸
@@ -63,8 +63,7 @@ export class LiquifyCPUManager {
// 新增:持续按压相关状态
this.pressStartTime = 0; // 按压开始时间
this.pressDuration = 0; // 按压持续时间
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)--废除使用固定角度
this.fixedRotationAngle = 0.32; // 固定旋转角度
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
this.lastApplyTime = 0; // 上次应用时间
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
@@ -190,7 +189,7 @@ export class LiquifyCPUManager {
this.isHolding = true;
// 启动持续效果定时器(对于所有模式都支持持续按压)
// this.startContinuousEffect();
this.startContinuousEffect();
console.log(`开始液化操作,初始点: (${x}, ${y})`);
}
@@ -221,6 +220,7 @@ export class LiquifyCPUManager {
// 新增:启动持续效果
startContinuousEffect() {
this.stopContinuousEffect(); // 先停止已有的定时器
this.continuousTimer = setInterval(() => {
if (this.isHolding && this.initialized) {
// 更新持续时间
@@ -273,6 +273,7 @@ export class LiquifyCPUManager {
*/
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
if (!this.currentImageData) return;
const data = this.currentImageData.data;
const width = this.currentImageData.width;
const height = this.currentImageData.height;
@@ -285,7 +286,6 @@ export class LiquifyCPUManager {
const rotationAngle =
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
console.log("持续应用旋转效果");
// 累积旋转角度 - 关键:这确保了持续旋转效果
this.accumulatedRotation += rotationAngle;
@@ -309,14 +309,13 @@ export class LiquifyCPUManager {
// 计算旋转后的源位置 - 关键算法
const angle = Math.atan2(dy, dx);
// const newAngle = angle + this.accumulatedRotation * falloff;
const newAngle = angle + (isClockwise ? this.fixedRotationAngle : -this.fixedRotationAngle) * falloff;
const newAngle = angle + this.accumulatedRotation * falloff;
const sourceX = centerX + Math.cos(newAngle) * distance;
const sourceY = centerY + Math.sin(newAngle) * distance;
// 双线性插值采样 - 确保像素连续性
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -377,7 +376,7 @@ export class LiquifyCPUManager {
const sourceY = centerY + dy * scale;
// 双线性插值采样
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -402,17 +401,16 @@ export class LiquifyCPUManager {
*/
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
if (!this.currentImageData) return;
const data = this.currentImageData.data;
const width = this.currentImageData.width;
const height = this.currentImageData.height;
const tempData = new Uint8ClampedArray(data);
// 计算推拉方向
const deltaX = this.currentMouseX - this.lastMouseX;
const deltaY = this.currentMouseY - this.lastMouseY;
const deltaX = this.currentMouseX - this.initialMouseX;
const deltaY = this.currentMouseY - this.initialMouseY;
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
this.lastMouseX = this.currentMouseX;
this.lastMouseY = this.currentMouseY;
const processRadius = Math.min(radius, Math.min(width, height) / 2);
const minX = Math.max(0, Math.floor(centerX - processRadius));
@@ -428,7 +426,6 @@ export class LiquifyCPUManager {
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
// 此处循环4万次
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -445,7 +442,7 @@ export class LiquifyCPUManager {
const sourceX = x - pushX;
const sourceY = y - pushY;
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -464,9 +461,9 @@ export class LiquifyCPUManager {
// 有拖拽时的推拉效果
const dirX = deltaX / dragLength;
const dirY = deltaY / dragLength;
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
// 此处循环4万次
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -476,13 +473,13 @@ export class LiquifyCPUManager {
const falloff = 1 - normalizedDistance * normalizedDistance;
const factor = falloff * strength;
const offsetX = dirX * factor * Math.min(dragLength * 2, 30);
const offsetY = dirY * factor * Math.min(dragLength * 2, 30);
const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15);
const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15);
const sourceX = x - offsetX;
const sourceY = y - offsetY;
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -530,7 +527,7 @@ export class LiquifyCPUManager {
break;
case this.modes.PUSH:
// this._applyEnhancedPushDeformation(x, y, radius, strength);
this._applyEnhancedPushDeformation(x, y, radius, strength);
break;
default: {
@@ -556,53 +553,101 @@ export class LiquifyCPUManager {
}
/**
* 双线性插值函数
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
* @param {number} x X坐标
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
* 应用液化变形 - 主要入口,集成增强算法
*/
_bilinearInterpolate(data, width, height, x, y) {
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = Math.min(width - 1, x1 + 1);
const y2 = Math.min(height - 1, y1 + 1);
// applyDeformation(x, y) {
// if (!this.initialized || !this.originalImageData) {
// console.warn("液化管理器未初始化或缺少必要数据");
// return this.currentImageData;
// }
const dx = x - x1;
const dy = y - y1;
const dx1 = 1 - dx;
const dy1 = 1 - dy;
const index1 = (y1 * width + x1) * 4;
const index2 = (y1 * width + x2) * 4;
const index3 = (y2 * width + x1) * 4;
const index4 = (y2 * width + x2) * 4;
const r =
data[index1] * dx1 * dy1 +
data[index2] * dx * dy1 +
data[index3] * dx1 * dy +
data[index4] * dx * dy;
const g =
data[index1 + 1] * dx1 * dy1 +
data[index2 + 1] * dx * dy1 +
data[index3 + 1] * dx1 * dy +
data[index4 + 1] * dx * dy;
const b =
data[index1 + 2] * dx1 * dy1 +
data[index2 + 2] * dx * dy1 +
data[index3 + 2] * dx1 * dy +
data[index4 + 2] * dx * dy;
const a =
data[index1 + 3] * dx1 * dy1 +
data[index2 + 3] * dx * dy1 +
data[index3 + 3] * dx1 * dy +
data[index4 + 3] * dx * dy;
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a)];
}
// // 更新鼠标位置
// this.currentMouseX = x;
// this.currentMouseY = y;
// // 计算拖拽参数
// const deltaX = this.currentMouseX - this.initialMouseX;
// const deltaY = this.currentMouseY - this.initialMouseY;
// this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// this.dragAngle = Math.atan2(deltaY, deltaX);
// // 获取当前参数
// const { size, pressure, power } = this.params;
// const mode = this.currentMode;
// const radius = size;
// const strength = pressure * power;
// // 根据模式选择算法
// const pixelModes = [
// this.modes.CLOCKWISE,
// this.modes.COUNTERCLOCKWISE,
// this.modes.PINCH,
// this.modes.EXPAND,
// this.modes.PUSH,
// ];
// if (pixelModes.includes(mode)) {
// // 使用增强的像素算法
// switch (mode) {
// case this.modes.CLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
// break;
// case this.modes.COUNTERCLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
// break;
// case this.modes.PINCH:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
// break;
// case this.modes.EXPAND:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
// break;
// case this.modes.PUSH:
// this._applyEnhancedPushDeformation(x, y, radius, strength);
// break;
// }
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
// return this.currentImageData;
// } else {
// // 使用原有的网格算法处理其他模式
// if (!this.mesh) {
// console.warn("网格未初始化");
// return this.currentImageData;
// }
// const finalStrength = (strength * this.config.maxStrength) / 100;
// // 应用变形
// this._applyDeformation(
// x,
// y,
// radius,
// finalStrength,
// mode,
// this.params.distortion,
// );
// // 平滑处理
// if (this.config.smoothingIterations > 0) {
// this._smoothMesh();
// }
// // 更新图像数据
// const result = this._applyMeshToImage();
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
// return result;
// }
// }
/**
* 三次插值实现 - 确保正确处理Alpha通道
* 双线性插值采样 - 用于像素级算法
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
@@ -610,9 +655,19 @@ export class LiquifyCPUManager {
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
_bilinearSample(data, width, height, x, y) {
return this._bicubicInterpolate(data, width, height, x, y);
}
/**
* 双三次插值实现 - 确保正确处理Alpha通道
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
* @param {number} x X坐标
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
_bicubicInterpolate(data, width, height, x, y) {
// return this._bilinearInterpolate(data, width, height, x, y);
// 获取周围16个像素点
const x1 = Math.floor(x) - 1;
const y1 = Math.floor(y) - 1;

View File

@@ -310,7 +310,7 @@ export class LiquifyWebGLManager {
this.isHolding = true;
// 启动持续效果定时器
// this.startContinuousEffect();
this.startContinuousEffect();
console.log(`WebGL液化开始初始点: (${x}, ${y})`);
}

View File

@@ -79,9 +79,6 @@ export class LayerSort {
} else if (layer.isFixed && layer.fabricObject) {
// 固定图层对象
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (layer.isFixedOther && layer.fabricObject) {
// 其他固定图层对象
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (!layer.isBackground && !layer.isFixed) {
// 普通图层
currentZIndex = this.processLayerObjects(

View File

@@ -1,37 +0,0 @@
class EventManager {
constructor() {
this.eventMap = {};
}
/**
* 注册事件
* @param {string} eventName - 事件名称
* @param {function} callback - 事件回调函数
*/
on(eventName, callback) {
if (!this.eventMap[eventName]) {
this.eventMap[eventName] = [];
}
this.eventMap[eventName].push(callback);
}
/**
* 触发事件
* @param {string} eventName - 事件名称
* @param {...any} args - 事件参数
*/
emit(eventName, ...args) {
if (this.eventMap[eventName]) {
this.eventMap[eventName].forEach(callback => callback(...args));
}
}
/**
* 移除事件
* @param {string} eventName - 事件名称
* @param {function} callback - 事件回调函数
*/
off(eventName, callback) {
if (this.eventMap[eventName]) {
this.eventMap[eventName] = this.eventMap[eventName].filter(cb => cb !== callback);
}
}
}
export default new EventManager();

View File

@@ -429,8 +429,7 @@ export function objectIsInCanvas(canvas, targetObj) {
}
const targetId = targetObj.id;
const targetLayerId = targetObj.layerId;
if (!targetId && !targetLayerId) {
if (!targetId) {
return { flag: false, object: null, parent: null };
}
@@ -438,11 +437,7 @@ export function objectIsInCanvas(canvas, targetObj) {
const topLevelObjects = canvas.getObjects();
// 直接在顶层查找
const directMatch = topLevelObjects.find((obj) => {
const isId = !targetId ? true : obj.id === targetId;
const isLayerId = !targetLayerId ? true : obj.layerId === targetLayerId;
return isId && isLayerId;
});
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
if (directMatch) {
return { flag: true, object: directMatch, parent: null };
}
@@ -505,22 +500,6 @@ export function findObjectById(canvas, objectId) {
return { object: result.object, parent: result.parent };
}
/**
* 通过layerID查找对象增强版
* @param {fabric.Canvas} canvas 画布实例
* @param {string} layerId 图层ID
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
*/
export function findObjectByLayerId(canvas, layerId) {
if (!canvas || !layerId) {
return { object: null, parent: null };
}
const result = objectIsInCanvas(canvas, { layerId: layerId });
return { object: result.object, parent: result.parent };
}
/**
* 安全移除画布对象(包括组内对象)
* @param {fabric.Canvas} canvas 画布实例
@@ -759,252 +738,3 @@ export function getLayerObjectsZIndex(canvas, layerId) {
const allInfo = getAllObjectsZIndex(canvas);
return allInfo.filter((info) => info.layerId === layerId);
}
/**
* 计算两点之间的角度
* @param {number} x1 第一个点的x坐标
* @param {number} y1 第一个点的y坐标
* @param {number} x2 第二个点的x坐标
* @param {number} y2 第二个点的y坐标
* @returns {number} 角度值(-90 - 270度
*/
export function calculateAngle(x1, y1, x2, y2, int = false) {
// 计算两点之间的差值
const deltaX = x2 - x1;
const deltaY = y2 - y1;
// 使用Math.atan2计算弧度然后转换为角度
let angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90;
return int ? Math.round(angle) : angle;
}
/**
* 通过角度计算直线上的两点坐标返回0-1范围的坐标
* @param {number} angle 角度值0-360度
* @returns {{x1: number, y1: number, x2: number, y2: number}} 包含两个点坐标
*/
export function calculateLinePoints(angle) {
// 将角度转换为弧度
const radian = (angle - 90) * Math.PI / 180;
// 计算直线上的两点坐标
const x1 = 0.5 - 0.5 * Math.cos(radian);
const y1 = 0.5 - 0.5 * Math.sin(radian);
const x2 = 0.5 + 0.5 * Math.cos(radian);
const y2 = 0.5 + 0.5 * Math.sin(radian);
return {x1, y1, x2, y2};
}
export function rgbaToHex(rgba){
const r = rgba.r.toString(16).padStart(2, "0");
const g = rgba.g.toString(16).padStart(2, "0");
const b = rgba.b.toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
export function fillToPallet(fill) {
if(!fill.coords || !fill.colorStops) return {};
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
const colors = new Set();
// console.log("==========fill", fill);
const gradientList = fill.colorStops.map((stop) => {
colors.add(stop.color);
const rgbas = stop.color.replace("rgb(", "").replace("rgba(", "").replace(")", "").split(", ");
const rgba = {
r: parseInt(rgbas[0]),
g: parseInt(rgbas[1]),
b: parseInt(rgbas[2]),
a: parseFloat(rgbas[3]),
};
if(isNaN(rgba.r)) rgba.r = 0;
if(isNaN(rgba.g)) rgba.g = 0;
if(isNaN(rgba.b)) rgba.b = 0;
if(isNaN(rgba.a)) rgba.a = 1;
return {
rgba: rgba,
left: parseInt(stop.offset * 100) + "%",
};
});
const isGradient = colors.size > 1;
if(isGradient) {
return {
// hex: rgbaToHex(gradientList[0].rgba),
rgba: gradientList[0].rgba,
gradient:{ angle, selectIndex: 0, gradientShow: true, gradientList },
};
} else {
return {
hex: rgbaToHex(gradientList[0].rgba),
rgba: gradientList[0].rgba,
};
}
}
export function palletToFill(pallet) {
const fill = {
coords: calculateLinePoints(0),
colorStops: [
{ offset: 0, color: "rgba(0, 0, 0, 0)" },
{ offset: 1, color: "rgba(0, 0, 0, 0)" }
]
}
if(pallet?.gradient){
let obj = pallet.gradient;
fill.coords = calculateLinePoints(obj.angle);
if(obj.gradientList.length >= 2){
fill.colorStops = obj.gradientList.map(item => ({
offset: parseInt(item.left) / 100,
color: `rgba(${item.rgba.r}, ${item.rgba.g}, ${item.rgba.b}, ${item.rgba.a})`,
}));
}
}else if(pallet?.rgba?.hasOwnProperty("r") && pallet?.rgba?.hasOwnProperty("g") && pallet?.rgba?.hasOwnProperty("b")){
let rgba = pallet.rgba;
fill.colorStops = [
{ offset: 0, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` },
{ offset: 1, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` }
]
}
return fill;
}
export function fillToCssStyle(fill) {
if(!fill.coords || !fill.colorStops) return "";
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
if(fill.colorStops.every(v => v.color === fill.colorStops[0].color)){
return fill.colorStops[0].color;
}else{
var str = "linear-gradient(" + angle + "deg, ";
fill.colorStops.forEach((v) => {
str += `${v.color} ${v.offset * 100}%, `
})
return str.slice(0, -2) + ")";
}
}
/**
* 根据左上角坐标计算旋转后的新坐标
* @param {number} W - 宽度
* @param {number} H - 高度
* @param {number} currentX - 当前左上角x坐标
* @param {number} currentY - 当前左上角y坐标
* @param {number} currentAngleDeg - 当前角度(度)
* @param {number} newAngleDeg - 新角度(度)
* @returns {Object} 旋转后的左上角坐标 {x, y}
*/
export 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 };
}
/**
* 根据左上角坐标计算中心点坐标
* @param {number} W - 宽度
* @param {number} H - 高度
* @param {number} currentX - 当前左上角x坐标
* @param {number} currentY - 当前左上角y坐标
* @param {number} currentAngleDeg - 当前角度(度)
* @returns {Object} 中心点坐标 {x, y}
*/
export function calculateCenterPoint(W, H, currentX, currentY, currentAngleDeg) {
const currentAngle = (currentAngleDeg * Math.PI) / 180;
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;
return { x: Cx, y: Cy };
}
/**
* 创建缩放+旋转的变换矩阵
* @param {number} scale - 缩放比例
* @param {number} angle - 旋转角度(度)
* @returns {Array} 变换矩阵 [a, b, c, d, e, f]
*/
export function createPatternTransform(scale, angle) {
// return fabric.util.composeMatrix({
// scaleX: scale,
// scaleY: scale,
// angle: angle,
// });
const angle_ = angle * Math.PI / 180;
const cos = Math.cos(angle_);
const sin = Math.sin(angle_);
// 先缩放,后旋转
return [
scale * cos, // a
scale * sin, // b
-scale * sin, // c
scale * cos, // d
0, // e (水平位移)
0 // f (垂直位移)
];
}
/**
* 获取变换矩阵的缩放、旋转
* @param {Array} Transform - 变换矩阵、
* @returns {Object} 缩放、旋转角度 {scale, angle}
*/
export function getTransformScaleAngle(Transform) {
const a = Transform[0];
const b = Transform[1];
const c = Transform[2];
const d = Transform[3];
const scale = Math.sqrt(a * a + b * b);
const angle = Math.round(Math.atan2(b, a) * 180 / Math.PI);
return { scale, angle };
}
/**
* 图片转换为canvas
* @param {String} base64 - 图片base64编码
* @param {Number} scale - 缩放比例
* @param {Boolean} sr - 缩放反转默认false
* @returns {Promise<HTMLCanvasElement>} canvas元素
*/
export async function base64ToCanvas(base64, scale = 1, sr = false) {
return new Promise((resolve, reject) => {
const image = new Image();
image.src = base64;
image.crossOrigin = 'anonymous';
image.onload = () => {
image.width = image.width;
image.height = image.height;
const canvas = document.createElement('canvas');
const width = (sr ? image.width / scale : image.width * scale);
const height = sr ? image.height / scale : image.height * scale;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.drawImage(image, 0, 0, width, height);
resolve(canvas);
};
image.onerror = reject;
});
}

View File

@@ -5,8 +5,8 @@
*/
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
aligningLineOffset = 1,
aligningLineMargin = 1,
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
aligningLineColor = "rgb(0,255,0)",
viewportTransform,
@@ -14,9 +14,9 @@ function initAligningGuidelines(canvas) {
function drawVerticalLine(coords) {
drawLine(
coords.x,
coords.x + 0.5,
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
coords.x,
coords.x + 0.5,
coords.y2 > coords.y1 ? coords.y2 : coords.y1
);
}
@@ -24,9 +24,9 @@ function initAligningGuidelines(canvas) {
function drawHorizontalLine(coords) {
drawLine(
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
coords.y,
coords.y + 0.5,
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
coords.y
coords.y + 0.5
);
}
@@ -351,7 +351,7 @@ export function initCenteringGuidelines(canvas) {
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = {},
canvasHeightCenterMap = {},
centerLineMargin = 1,
centerLineMargin = 4,
centerLineColor = "rgba(255,0,241,0.5)",
centerLineWidth = 1,
ctx = canvas.getSelectionContext(),

View File

@@ -18,16 +18,6 @@ export const LayerType = {
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
};
/**
* 特殊图层ID
*/
export const SpecialLayerId = {
SPECIAL_GROUP: "group_special", // 特殊组
COLOR: "special_color", // 颜色图层
}
/**
* 画布操作模式枚举draw(绘画)、select(选择)、pan(拖拽)....
*/
@@ -188,17 +178,12 @@ export function createLayer(options = {}) {
locked: options.locked !== undefined ? options.locked : false,
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
isHidenDragHandle: options.isHidenDragHandle || false,
isDisableUnlock: options.isDisableUnlock || false,
isFixedOther: options.isFixedOther || false,
isFixedClipMask: options.isFixedClipMask || false,
// 确保不是背景图层
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
fabricObject: options.fabricObject || null,
// 嵌套结构 - 适用于组图层
children: options.children || [],

View File

@@ -155,19 +155,15 @@ export function validateLayerAssociations(layers, canvasObjects) {
/**
* 简化layers对象属性只保留必要的属性
* @param {Array} layers 图层数组
* @param {Array} excludedLayers 排除的图层ID数组
* @returns {Array} 简化后的图层数组
*/
export function simplifyLayers(layers, excludedLayers = []) {
export function simplifyLayers(layers) {
if (!layers || !isArray(layers)) {
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
return [];
}
return layers.map((layer) => {
// 检查是否在排除列表中
if (excludedLayers && excludedLayers.includes(layer.id)) return null;
const simplifiedLayer = {
id: layer.id,
name: layer.name,
@@ -176,10 +172,6 @@ export function simplifyLayers(layers, excludedLayers = []) {
opacity: layer.opacity,
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
isFixedOther: layer.isFixedOther || false,
isFixedClipMask: layer.isFixedClipMask || false,
isHidenDragHandle: layer.isHidenDragHandle || false,
isDisableUnlock: layer.isDisableUnlock || false,
clippingMask:
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
layer.clippingMask ||
@@ -215,11 +207,10 @@ export function simplifyLayers(layers, excludedLayers = []) {
fill: layer?.fill || null,
fillColor: layer.fillColor,
selectObject: layer.selectObject,
blendMode: layer.blendMode || null,
};
return simplifiedLayer;
}).filter((layer) => !!layer);
});
}
/**

View File

@@ -7,106 +7,55 @@ import { fabric } from "fabric-with-all";
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
*/
export async function restoreFabricObject(serializedObject, canvas) {
return new Promise((resolve, reject) => {
const objectType = serializedObject.type;
// 定义恢复后的处理函数
const handleRestoredObject = (fabricObject) => {
if (!fabricObject) {
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
return;
}
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
return new Promise((resolve, reject) => {
const objectType = serializedObject.type;
// 定义恢复后的处理函数
const handleRestoredObject = (fabricObject) => {
if (!fabricObject) {
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
return;
}
// 更新坐标
fabricObject.setCoords();
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
// 添加到画布
// canvas.add(fabricObject);
// 更新坐标
fabricObject.setCoords();
resolve(fabricObject);
};
// 添加到画布
// canvas.add(fabricObject);
// 根据类型选择恢复方法
switch (objectType) {
case "rect":
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
break;
case "circle":
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
break;
case "path":
fabric.Path.fromObject(serializedObject, handleRestoredObject);
break;
case "image":
fabric.Image.fromObject(serializedObject, handleRestoredObject);
break;
case "group":
fabric.Group.fromObject(serializedObject, handleRestoredObject);
break;
default:
// 使用通用方法
fabric.util.enlivenObjects([serializedObject], (objects) => {
if (objects && objects[0]) {
handleRestoredObject(objects[0]);
} else {
reject(new Error("对象恢复失败"));
}
});
}
});
resolve(fabricObject);
};
// 根据类型选择恢复方法
switch (objectType) {
case "rect":
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
break;
case "circle":
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
break;
case "path":
fabric.Path.fromObject(serializedObject, handleRestoredObject);
break;
case "image":
fabric.Image.fromObject(serializedObject, handleRestoredObject);
break;
case "group":
fabric.Group.fromObject(serializedObject, handleRestoredObject);
break;
default:
// 使用通用方法
fabric.util.enlivenObjects([serializedObject], (objects) => {
if (objects && objects[0]) {
handleRestoredObject(objects[0]);
} else {
reject(new Error("对象恢复失败"));
}
});
}
});
}
/**
* 获取对象黑白通道画布
* @param {fabric.Object} object - 要处理的 fabric 对象
* @param {ImageData} revData - 相反的ImageData白通道的相同位置是否为透明revData为白色为透明黑色为不透明
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
*/
export function getObjectAlphaToCanvas(object, revData) {
const image = object.getElement();
const { width, height } = image;
if (!width || !height) {
console.warn("对象没有元素");
return null;
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, width, height);
const data = ctx.getImageData(0, 0, width, height);
for (let i = 0; i < data.data.length; i += 4) {
const r = data.data[i + 0];
const g = data.data[i + 1];
const b = data.data[i + 2];
const a = data.data[i + 3];
const revR = revData?.data[i + 0] || 0;
const revG = revData?.data[i + 1] || 0;
const revB = revData?.data[i + 2] || 0;
const revA = revData?.data[i + 3] || 0;
if (r || g || b || a) {
if (revR || revG || revB || revA) {
data.data[i + 0] = 0;
data.data[i + 1] = 0;
data.data[i + 2] = 0;
data.data[i + 3] = 0;
} else {
data.data[i + 0] = 255;
data.data[i + 1] = 255;
data.data[i + 2] = 255;
data.data[i + 3] = 255;
}
} else {
data.data[i + 0] = 0;
data.data[i + 1] = 0;
data.data[i + 2] = 0;
data.data[i + 3] = 0;
}
}
ctx.clearRect(0, 0, width, height);
ctx.putImageData(data, 0, 0);
return canvas;
}

View File

@@ -1,6 +1,6 @@
// 栅格化帮助
import { fabric } from "fabric-with-all";
import { SpecialLayerId } from "./layerHelper";
/**
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
@@ -68,7 +68,7 @@ export const createRasterizedImage = async ({
isReturenDataURL,
});
} catch (error) {
console.warn("创建栅格化图像失败:", error);
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
@@ -163,7 +163,7 @@ const createClippedObjects = async ({
console.log("✅ 返回裁剪后的fabric对象已恢复到优化后的原始大小和位置");
return fabricImage;
} catch (error) {
console.warn("创建裁剪对象失败:", error);
console.error("创建裁剪对象失败:", error);
throw error;
}
};
@@ -184,16 +184,10 @@ const createClippedDataURLByCanvas = async ({
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
// 使用优化后的边界计算,确保包含描边区域
// const optimizedBounds = calculateOptimizedBounds(
// clippingObject,
// fabricObjects
// );
const optimizedBounds = {
left: clippingObject.left - clippingObject.width / 2,
top: clippingObject.top - clippingObject.height / 2,
width: clippingObject.width,
height: clippingObject.height,
}
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
@@ -691,16 +685,6 @@ const cloneObjectAsync = (obj) => {
return new Promise((resolve, reject) => {
obj.clone((cloned) => {
if (cloned) {
cloned.set({
scaleX: obj.scaleX,
scaleY: obj.scaleY,
top: obj.top,
left: obj.left,
width: obj.width,
height: obj.height,
zoomX: obj.zoomX,
zoomY: obj.zoomY,
})
resolve(cloned);
} else {
reject(new Error("对象克隆失败"));
@@ -855,8 +839,9 @@ const renderContentToImage = async ({
});
// 克隆并添加所有需要渲染的对象
for (let obj of fabricObjects) {
let clonedObj = await cloneObjectAsync(obj);
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
// 调整对象位置:将选区左上角作为新的原点(0,0)
clonedObj.set({
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
@@ -868,19 +853,19 @@ const renderContentToImage = async ({
});
// 如果有裁剪路径,也需要调整裁剪路径
if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) {
if (clonedObj.clipPath) {
clonedObj.clipPath.set({
left: (clonedObj.clipPath.left - selectionBounds.left) * qualityMultiplier,
top: (clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier,
left:
(clonedObj.clipPath.left - selectionBounds.left) *
qualityMultiplier,
top:
(clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clonedObj.clipPath.scaleX || 1) * qualityMultiplier,
scaleY: (clonedObj.clipPath.scaleY || 1) * qualityMultiplier,
});
clonedObj.clipPath.setCoords(); // 更新裁剪路径坐标
}
// if(obj.globalCompositeOperation === "multiply"){
// clonedObj.clipPath = null;
// }
console.log("==========", obj.id, obj.layerName);
contentCanvas.add(clonedObj);
}
@@ -1254,7 +1239,7 @@ const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
return optimizedBounds;
} catch (error) {
console.warn("计算优化边界框失败:", error);
console.error("计算优化边界框失败:", error);
// 返回原始计算方式作为备选
return clippingObject.getBoundingRect(true, true);
}

View File

@@ -101,8 +101,7 @@ onUnmounted(() => {
:style="tool.style"
@click="handleClick"
>
<SvgIcon v-if="tool.icon" :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
<span v-else>{{ tool.label }}</span>
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
<teleport to="body" v-if="tipBody">
<div class="tool-tooltip" :id="tipId">{{ t(tool.title) }}</div>
</teleport>

View File

@@ -53,7 +53,7 @@ const changeFixedImage = () => {
canvasEditor.value.changeFixedImage(changeImageUrl);
};
const frontBackChange = (value) =>{
console.log("==========红绿图导出url", value)
console.log(value)
}
// 组件挂载时绑定键盘事件

View File

@@ -9,8 +9,8 @@ import ToolButton from "@/component/Canvas/ExistsImageList/ToolButton.vue";
const canvasEditor = ref();
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
const clothingImageUrl = "/src/assets/images/canvas/xiangao.png";
const clothingImageUrlInit = "/src/assets/images/canvas/xiangaofenge.png";
const clothingImageUrl = "/src/assets/work/3.PNG";
const clothingImageUrlInit = "/src/assets/work/5.PNG";
const imageData = [
{
@@ -71,10 +71,8 @@ const editorConfig = {
const exportImage = async () => {
if (canvasEditor.value) {
const base64 = await canvasEditor.value.exportImage({
isContainFixed: false, // 是否导出底图
isContainFixedOther: false, // 是否导出其他固定图层
isContainFixed: true, // 是否导出底图
isContainBg: false, // 是否导出背景
isEnhanceImg: false, // 是否导出增强图片
});
// 模拟下载图片
@@ -86,30 +84,6 @@ const exportImage = async () => {
document.body.removeChild(link); // 下载后移除链接元素
}
};
// 导出颜色图层
const exportColorLayer = async () => {
if (canvasEditor.value) {
const colorLayer = await canvasEditor.value.exportColorLayer();
console.log("导出颜色图层:",colorLayer);
// 模拟下载图片
const link = document.createElement("a");
link.href = colorLayer.base64;
link.download = "canvas_image.png"; // 设置下载文件名
document.body.appendChild(link);
link.click(); // 触发下载
document.body.removeChild(link); // 下载后移除链接元素
}
};
// 导出所有信息
const exportExtraInfo = async () => {
if (canvasEditor.value) {
const extraInfo = await canvasEditor.value.exportExtraInfo();
console.log("==========导出信息:", extraInfo);
}
};
const changeCanvas = (command) => {
console.log(command);
@@ -132,6 +106,32 @@ const loadImageUrlToLayer = async () => {
}
};
// 自定义工具配置相关
const customToolsList = ref([
{
id: "exportPNG",
title: "导出PNG", //导出画布图片
action: exportAsPNG,
icon: { name: "CExport", size: "24" },
class: "export-btn",
},
{
id: "saveCanvas",
title: "保存画布",
action: saveCanvas,
icon: { name: "CBottom", size: "24" },
class: "save-btn",
},
{
id: "readCanvas",
title: "读取画布",
action: canvasProject,
icon: { name: "CMiniMap", size: "24" },
class: "clear-btn",
},
]);
// 自定义工具方法
function exportAsPNG() {
console.log("导出PNG");
@@ -201,130 +201,10 @@ const canvasInit = () => {
};
const frontBackChange =(value)=>{
console.log(value)
}
// 自定义工具配置相关
const customToolsList = ref([
{
id: "exportColorLayer",
title: "导出颜色图层",
action: exportColorLayer,
label: "导颜",
class: "export-btn",
},
{
id: "exportExtraInfo",
title: "导出印花颜色等信息",
action: exportExtraInfo,
label: "导E",
class: "export-btn",
},
{
id: "exportPNG",
title: "导出PNG", //导出画布图片
action: exportAsPNG,
icon: { name: "CExport", size: "24" },
class: "export-btn",
},
{
id: "saveCanvas",
title: "保存画布",
action: saveCanvas,
icon: { name: "CBottom", size: "24" },
class: "save-btn",
},
{
id: "readCanvas",
title: "读取画布",
action: canvasProject,
icon: { name: "CMiniMap", size: "24" },
class: "clear-btn",
},
{
id: "loadImageUrlToLayer",
title: "添加画布图片",
action: loadImageUrlToLayer,
label: "🎨",
class: "export-btn",
},
{
id: "redGreenExample",
title: "红绿图模式",
action: () => switchView('redGreenExample'),
label: "红",
class: "export-btn",
},
{
id: "canvasEditor",
title: "普通模式",
action: () => switchView('canvasEditor'),
label: "普",
class: "export-btn",
},
{
id: "changeFixedImage",
title: "更换底图",
action: changeFixedImage,
label: "更",
class: "export-btn",
},
{
id: "exportJSON",
title: "导出JSON",
action: exportJSON,
label: "导J",
class: "export-btn",
},
{
id: "copyJSON",
title: "复制JSON",
action: copyJSON,
label: "复J",
class: "export-btn",
},
{
id: "getLayers",
title: "查询图层",
action: getLayers,
label: "查L",
class: "export-btn",
},
]);
const otherData = {
color: {rgba: {r:255,g:0,b:0,a:1}},
printObject: {
prints: [
{
ifSingle: false,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780],
scale: [0.5 * 0.7, 0.272541 * 0.7],
angle: 0,
},
{
ifSingle: true,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780],
scale: [0.5 * 0.7, 0.272541 * 0.7],
angle: 0,
},
{
ifSingle: true,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [300, 500],
scale: [0.5 * 0.4, 0.272541 * 0.4],
angle: 0,
}
]
},
}
const isShowLeft = ref(true);
</script>
<template>
@@ -352,10 +232,8 @@ const otherData = {
ref="canvasEditor"
key="canvasEditor"
v-if="currentView === 'canvasEditor'"
:clothingImageUrl="clothingImageUrl"
:clothingImageUrl2="clothingImageUrlInit"
:otherData="otherData"
:config="editorConfig"
:clothingImageUrl="clothingImageUrl"
:clothing-image-opts="{
imageMode: 'contains', // 设置底图包含在画布内
}"
@@ -372,15 +250,46 @@ const otherData = {
<template #customToolsBottom="{ toolButtonProps }">
<!-- 分隔线 -->
<div class="tool-separator"></div>
<!-- 自定义工具按钮 -->
<!-- 自定义工具按钮 -->
<ToolButton
v-for="tool in customToolsList"
:key="tool.id"
:tool="tool"
:active-tool="toolButtonProps.activeTool"
@click="handleCustomToolClick"
tipBody
/>
<!-- 也可以直接使用普通的按钮 -->
<div class="custom-tool-btn" @click="loadImageUrlToLayer">
<span>🎨</span>
<div class="tool-tooltip">添加画布图片</div>
</div>
<div class="custom-tool-btn" @click="switchView('redGreenExample')">
<span></span>
<div class="tool-tooltip">红绿图模式</div>
</div>
<div class="custom-tool-btn" @click="switchView('canvasEditor')">
<span></span>
<div class="tool-tooltip">普通模式</div>
</div>
<div class="custom-tool-btn" @click="changeFixedImage">
<span></span>
<div class="tool-tooltip">更换底图</div>
</div>
<div class="custom-tool-btn" @click="exportJSON">
<span></span>
<div class="tool-tooltip">导出JSON</div>
</div>
<div class="custom-tool-btn" @click="copyJSON">
<span></span>
<div class="tool-tooltip">复制JSON</div>
</div>
<div class="custom-tool-btn" @click="getLayers">
<span></span>
<div class="tool-tooltip">查询图层</div>
</div>
</template>
</CanvasEditor>
</div>
@@ -515,17 +424,15 @@ body {
.tool-tooltip {
display: none;
pointer-events: none;
position: absolute;
writing-mode: vertical-rl; /* 竖直排列 */
text-orientation: upright; /* 保持文字正常显示 */// left: 100%;
left: 50%;
top: -0.8rem;
transform: translate(-50%, -100%);
background-color: rgba(0, 0, 0, 0.9);
left: 100%;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 0.8rem 0.4rem;
padding: 0.4rem 0.8rem;
border-radius: 0.4rem;
margin-left: 0.8rem;
white-space: nowrap;
font-size: 1.2rem;
z-index: 10;
@@ -534,12 +441,12 @@ body {
.tool-tooltip:before {
content: "";
position: absolute;
left: 50%;
bottom: 0;
transform: translate(-50%, 100%);
top: 50%;
right: 100%;
margin-top: -0.5rem;
border-width: 0.5rem;
border-style: solid;
border-color: rgba(0, 0, 0, 0.9) transparent transparent transparent;
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
}
/* 深色模式适配 */

View File

@@ -61,7 +61,7 @@
<model
ref="model"
:key="positionKey"
@addDetail="addDetail"
@canvasReload="canvasReload"
@detailEdit="detailEdit"
@addSketch="()=>isEditPattern.value = ''"
@@ -78,16 +78,7 @@
<div class="item detailRight" :class="{canvas:isEditPattern.value}">
<div class="submit">
</div>
<div class="contentRight" v-if="currentDetailType === 'sketch' && !selectDetail?.newDetail?.[currentDetailType] && !selectDetail.sketchString && !isEditPattern.value">
<img
style="width: 100%; height: 100%;object-fit: contain;"
:src="
'/image/toolsGuide/' +
(locale == 'ENGLISH' ? 'detailEN' : 'detailCN') +
'.png'
" alt="">
</div>
<div class="contentRight" v-else-if="currentDetailType && !isEditPattern.value">
<div class="contentRight" v-if="currentDetailType && !isEditPattern.value">
<detailRight ref="detailRight"></detailRight>
<div class="btn"
v-show="
@@ -111,7 +102,7 @@
</div> -->
</div>
</div>
<addDetails ref="addDetails" @setSloganData="setSloganData"></addDetails>
</a-modal>
<div class="mark_loading" v-show="loadingShow">
<a-spin size="large" />
@@ -137,18 +128,17 @@ import { useI18n } from 'vue-i18n'
import addDetails from '@/component/Detail/addDetails.vue'
export default defineComponent({
components:{
detailLeft,model,detailRight,canvasBox,addDetails
detailLeft,model,detailRight,canvasBox
},
emits:['destroy'],
setup(props,{emit}) {
const store = useStore();
const {locale} = useI18n()
const detailDom = reactive({
model:null,
canvasBox,
detailRight,
detailLeft:null as any,
addDetails:null as any,
})
const userDetail = computed(()=>{
return store.state.UserHabit.userDetail
@@ -516,17 +506,6 @@ export default defineComponent({
sessionStorage.setItem('revocation', JSON.stringify(revocation));
sessionStorage.setItem('oppositeRevocation',JSON.stringify([]));
}
const addDetail = () =>{
let addDetails:any = detailDom.addDetails
addDetails.init(detailData.selectDetail,'')
}
const setSloganData = (data:any)=>{
detailData.selectDetail.sketchString = data
if(detailData.currentDetailType == 'sketch' && detailData.selectDetail?.newDetail?.sketch){
detailData.selectDetail.newDetail.sketch = null
}
}
onMounted(()=>{
window.addEventListener('resize', handleResize);
})
@@ -539,7 +518,6 @@ export default defineComponent({
})
return{
locale,
...toRefs(detailDom),
...toRefs(detailData),
closeModal,
@@ -553,8 +531,6 @@ export default defineComponent({
canvasReload,
modelOnLoad,
sketchSysToLibrary,
addDetail,
setSloganData,
}
},

View File

@@ -5,17 +5,14 @@
<div class="canvasContent" ref="canvasContent">
<div class="content-bottom" ref="canvasContent">
<div class="contet">
<!-- :clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path" -->
<div class="canvas" v-if="currentView === 'canvasEditor'" @click.stop>
<editCanvas v-if="canvasLoad" :config="canvasConfig"
@canvasInit="canvasInit"
@changeCanvas="changeCanvas"
is-edit
:clothingImageUrl="selectDetail.path"
:clothingImageUrl2="selectDetail.undividedLayer"
:clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path"
showFixedLayer
:canvasJSON="canvasJSON"
:otherData="otherData"
:clothing-image-opts="{
imageMode:'contains',
}"
@@ -111,12 +108,6 @@ export default defineComponent({
canvasInstance:null as any,
canvasJSON:'',
hideCanvas: computed(()=>store.state.Workspace.projectPath !== route.fullPath),
otherData:computed(()=>({
canvasId: store.state.DesignDetail.selectDetail.canvasId,
color: store.state.DesignDetail.selectDetail.color,
printObject: store.state.DesignDetail.selectDetail.printObject,
trims: store.state.DesignDetail.selectDetail.trims,
})),
})
watch(()=>detailData.selectDetail,(newValue,oldValue)=>{
detailData.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
@@ -126,6 +117,7 @@ export default defineComponent({
provide('canvasType',detailData.canvasType)
const editFront = (str:any)=>{//编辑前后片
let canvasJSON = '' as any
if(detailData.currentView === 'canvasEditor'){
sessionStorage.setItem('sketchEdit',detailDom.editCanvas.getJSON())
@@ -238,7 +230,7 @@ export default defineComponent({
let size = {
...detailData.canvasConfig,
}
store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
segmentImage(value,full,size).then(async (rv)=>{
let front = detailData.frontBack.front[detailData.imgDomIndex]
let back = detailData.frontBack.back[detailData.imgDomIndex]
@@ -251,7 +243,7 @@ export default defineComponent({
let base64 = await resizeImageWithNativeCanvas(front.oldMaskUrl,value)
front.maskUrl = base64
back.imageUrl = rv.targetBackUrl
// store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
})
}
@@ -317,7 +309,7 @@ export default defineComponent({
sessionStorage.removeItem('frontBackEdit');
sessionStorage.removeItem('sketchEdit');
detailData.canvasLoad = false
// privewDetail()
privewDetail()
})
onMounted(()=>{
nextTick(async ()=>{

View File

@@ -1,5 +1,5 @@
<template>
<div class="pallet" ref="palletRef">
<div class="pallet">
<div class="palletColo" @click="openPallet">
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
{{ selectColor.hex }}
@@ -117,7 +117,6 @@ export default defineComponent({
})
const getpalletListDom = reactive({
})
const palletRef = ref(null)
watch(()=>palletData.color_,(newVal:any)=>{
if(!newVal?.rgba?.r)return
if(palletData.color?.gradient?.gradientShow){
@@ -222,7 +221,7 @@ export default defineComponent({
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
let gradientWidth = (document.querySelector('.pallet .color_setting_operate_bg') as any).clientWidth
let position = {
x:event.clientX,
left:event.target.style.left?event.target.style.left.split('%')[0]:0
@@ -277,8 +276,8 @@ export default defineComponent({
// 点击外部区域关闭颜色选择器
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
const palletColo = palletRef.value.querySelector('.palletColo');
const colorSettingBlock = document.querySelector('.color_setting_block');
const palletColo = document.querySelector('.palletColo');
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
if (palletData.palletShow && colorSettingBlock &&
@@ -295,7 +294,7 @@ export default defineComponent({
nextTick().then(()=>{
const backIcon = document.createElement('div');
backIcon.classList.add('vc-sketch-color-wrap')
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
let dropperDom = document.getElementsByClassName("pallet")?.[0]?.getElementsByClassName('vc-chrome-fields-wrap')[0]
dropperDom.appendChild(backIcon);
backIcon.addEventListener('click',async ()=>{
try {
@@ -323,7 +322,7 @@ export default defineComponent({
return{
...toRefs(palletData),
...toRefs(getpalletListDom),
palletRef,
openPallet,
selectImgItem,
setOperate,
@@ -615,7 +614,6 @@ export default defineComponent({
position: absolute;
content: "";
top: 0.2rem;
left: 0;
width: 1rem;
height: 1rem;
border-radius: 50%;

View File

@@ -6,6 +6,7 @@
<element v-show="currentDetailType == 'element'"></element>
<accessory v-show="currentDetailType == 'accessory'"></accessory>
<models v-show="currentDetailType == 'models'"></models>
<addDetails ref="addDetails" @setSloganData="setSloganData"></addDetails>
</div>
</template>
<script lang="ts">
@@ -21,12 +22,12 @@ import color from './colorBox/index.vue'
import element from './element.vue'
import accessory from './accessory.vue'
import models from './models.vue'
import addDetails from '@/component/Detail/addDetails.vue'
export default defineComponent({
components:{
sketch,print,color,element,models,accessory
sketch,print,color,addDetails,element,models,accessory
},
emit:['addDetail'],
setup(props,{emit}) {
const store = useStore();
const detailData = reactive({
@@ -44,7 +45,14 @@ export default defineComponent({
sketch:null as any,
})
const addDetail = () =>{
emit('addDetail')
let addDetails:any = getDetailListDom.addDetails
addDetails.init(detailData.selectDetail,'')
}
const setSloganData = (data:any)=>{
detailData.selectDetail.sketchString = data
if(detailData.currentDetailType == 'sketch' && detailData.selectDetail?.newDetail?.sketch){
detailData.selectDetail.newDetail.sketch = null
}
}
const sketchSysToLibrary = ()=>{//系统sketch添加到library更新library
getDetailListDom.sketch.sketchSysToLibrary()
@@ -55,6 +63,7 @@ export default defineComponent({
...toRefs(getDetailListData),
...toRefs(getDetailListDom),
addDetail,
setSloganData,
sketchSysToLibrary,
}
},

View File

@@ -5,7 +5,7 @@
<!-- <img :src="selectDetail?.sketchString?selectDetail?.sketchString:selectDetail.path" alt=""> -->
<img :src="selectDetail.path" alt="">
<!-- <img :src="selectDetail.sketchString || selectDetail.path" alt=""> -->
<!-- <i :title="$t('DesignDetail.editSketchTitle')" class="fi fi-rs-pencil-paintbrush" @click.stop="openAddDetail"></i> -->
<i :title="$t('DesignDetail.editSketchTitle')" class="fi fi-rs-pencil-paintbrush" @click.stop="openAddDetail"></i>
</div>
<div class="select_sketch" v-else>
<div>

View File

@@ -14,7 +14,6 @@
<i class="icon iconfont icon-chehui" @click="revocation"></i>
<i class="icon iconfont icon-fanchehui" @click="oppositeRevocation"></i>
<!-- 编辑 -->
<i class="fi fi-rs-pencil-paintbrush" :title="$t('DesignDetail.editSketchTitle')" :class="{'pointerEventsNone':!selectDetail?.id}" @click="()=>$emit('addDetail')"></i>
<i class="fi fi-rr-edit" :title="$t('DesignDetail.editTitle')" :class="{active:isEditPattern.value == 'canvasEditor','pointerEventsNone':!selectDetail?.id}" @click="showDesignImgDetail('canvasEditor')"></i>
<i class="icon iconfont icon-clothes" :title="$t('Canvas.editFrontBack')" style="font-size: 3.2rem;" @click="showDesignImgDetail('redGreenExample')" :class="{active:isEditPattern.value == 'redGreenExample','pointerEventsNone':!selectDetail?.id}"></i>
@@ -51,7 +50,7 @@ export default defineComponent({
components:{
position,modelNav
},
emits:['detailEdit','canvasReload','addSketch','revocation','oppositeRevocation','modelOnLoad','sketchSysToLibrary','addDetail'],
emits:['detailEdit','canvasReload','addSketch','revocation','oppositeRevocation','modelOnLoad','sketchSysToLibrary'],
setup(props,{emit}) {
const {t} = useI18n()
const store = useStore();

View File

@@ -30,7 +30,7 @@
:outputSize="option.size"
:outputType="option.outputType"
:auto-crop="option.autoCrop"
:fixedBox="!isRound"
:fixedBox="isRound"
:movable="true"
:fixed="isRound"
:auto-crop-width="option.autoCropWidth"
@@ -333,6 +333,7 @@ export default defineComponent({
.cut_picture_review_block{
width: 100%;
height: calc(100% - 6.8rem*1.2);
min-height: calc(100% - 6.8rem*1.2);
margin: 0 auto;
.next_step_button{
margin-top: 2rem;

View File

@@ -286,8 +286,7 @@
@click="generageAdd(item)"
:class="[
item.status != 'Success' ? 'hideEvents' : '',
item?.checked ? 'active' : '',
(type_.type2 == 'Printboard' && item?.imgUrl)? 'maskBg' : ''
item?.checked ? 'active' : ''
]"
>
<img v-if="item?.imgUrl" v-lazy="item.imgUrl" @click.stop="generageAdd(item)" />
@@ -1360,10 +1359,6 @@ export default defineComponent({
width: calc(25% - 2rem);
aspect-ratio: 1 / 1;
position: relative;
&.maskBg{
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23666' fill-opacity='0.4'%3E%3Crect x='20' width='20' height='20'/%3E%3Crect y='20' width='20' height='20'/%3E%3C/g%3E%3C/svg%3E");
background-size: 2rem 2rem; /* 调整图案密度 */
}
&.active {
opacity: 0.5;
// border: 2px solid;
@@ -1384,7 +1379,6 @@ export default defineComponent({
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
img {
// width: calc(10rem*1.2);
@@ -1392,7 +1386,6 @@ export default defineComponent({
width: 100%;
height: 100%;
object-fit: contain;
}
&:hover .delete_like_file_block {
// display: block;

View File

@@ -486,7 +486,7 @@ export default defineComponent({
.my_material_header_left{
margin-right: auto;
.select_block{
// border: calc(0.1rem* 1.2) solid #F1F1F1;
border: calc(0.1rem* 1.2) solid #F1F1F1;
margin-right: calc(2.3rem*1.2);
height: auto;
.ant-select-selector{

View File

@@ -34,8 +34,8 @@
</div>
</div>
<div class="layout_centent" :class="{active:flex_direction}" id="layoutCentent">
<div v-for="item,index in layoutList" :key="item" :class="moodbClassName[index]" class="modal_imgItem" v-layout="item" @mousedown="setpitch(item,index)" @touchstart="setpitch(item,index)" ref="content" :style="{'background-image':`url(${item.imgUrl})`,'transform':`scale(${item.zoom?item.zoom:1}) rotateZ(${item.angle?item.angle:0}deg)`}">
<!-- <img crossOrigin="anonymous" :src="item.imgUrl" :style="{'transform':`translate(-50%, -50%) scale(${item.zoom?item.zoom:1}) rotateZ(${item.angle?item.angle:0}deg)`}" draggable="false" :class="moodbClassName[index]" v-modelImg> -->
<div v-for="item,index in layoutList" :key="item" :class="moodbClassName[index]" class="modal_imgItem" v-layout="item" @mousedown="setpitch(item,index)" @touchstart="setpitch(item,index)" ref="content" >
<img crossOrigin="anonymous" :src="item.imgUrl" :style="{'transform':`translate(-50%, -50%) scale(${item.zoom?item.zoom:1}) rotateZ(${item.angle?item.angle:0}deg)`}" draggable="false" :class="moodbClassName[index]" v-modelImg>
<ul v-show="item.setPitch" class="layout_btn" >
<li class="layout_btn_top" v-compile.stop="'top'"></li>
<li class="layout_btn_bottom" v-compile.stop="'bottom'"></li>
@@ -736,7 +736,6 @@ export default defineComponent({
setmoodb(item:any){
this.moodbClassName = item
this.$emit('setmoodbClass',this.moodbClassName)
this.styleObj.class = this.moodbClassName
if(this.content){
for (item of (this.content as any)) {
item.classList.remove('active')
@@ -773,7 +772,7 @@ export default defineComponent({
initDomStyle(){
nextTick(()=>{
this.content.forEach((item:any,index:any) => {
if(this.styleObj.domStyle[index]?.left){
if(this.styleObj.domStyle[index]){
item.classList.add('active')
this.initStyle(item,this.styleObj.domStyle[index])
}
@@ -795,7 +794,7 @@ export default defineComponent({
})
},
initStyle(dom:any,style:any){
if(!style || !dom)return
if(!style)return
for (const [property, value] of Object.entries(style)) {
dom.style.setProperty(property, value);
@@ -807,7 +806,7 @@ export default defineComponent({
this.styleObj.domStyle.push(this.setStyle(item.style))
this.domObj.dom.forEach((domName:any,index:any) => {
let style = this.domObj.domStyle[index]
let dom = item.querySelector(domName) || item
let dom = item.querySelector(domName)
this.styleObj[style].push(this.setStyle(dom.style))
})
});
@@ -841,6 +840,7 @@ export default defineComponent({
let config:any = {headers:{'Content-Type':'multipart/form-data','Accept':'*/*' }}
Https.axiosPost(Https.httpUrls.elementUpload,param,config)
.then((rv: any) => {
// console.log(rv);
rv.imgUrl = rv.url
this.layout = false
this.loadingShow = false
@@ -1062,12 +1062,27 @@ export default defineComponent({
// height: 100%;
// }
overflow: hidden;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
&.active{
position: absolute;
}
img{
// object-fit: cover;
// width: 100%;
// height: 100%;
pointer-events: none;
float: left;
user-select:none;
-webkit-user-drag: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%) scale(1);
}
::selection {
// background: rgba(0,0,0,0);
// background: yellow;
}
}
}
.wh1{

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@
<i v-else class="fi fi-br-check" @click="editChek('brandSlogan')"></i>
</div>
</div>
<div class="compute" style="margin-left: auto;text-align: right;" v-show="fall?.list?.length > 0">
<div class="compute" style="flex:1;text-align: right;" v-show="fall?.list?.length > 0">
<!-- <div @click="setProgress(50)">123123123</div> -->
<div class="gallery_btn" @click="compute" :class="{'loading':!(schedule.num == 1||(schedule.num == 0 && !schedule.state))}">
{{$t('brandDNA.Compute')}}
@@ -546,7 +546,6 @@ export default defineComponent({
}
.gallery_btn{
&.loading{
width: min-content;
pointer-events: none;
color: #5F5F5F;
}

View File

@@ -6,7 +6,7 @@
<!-- <div class="icon" @click="toGmailLogin"> -->
<div class="icon">
<img src="@/assets/images/loginPage/gmailIcon.svg" alt="">
<span>{{ displayText }}</span>
<span>{{ $props.text }}</span>
</div>
</div>
</template>
@@ -21,7 +21,7 @@
props: {
text: {
type: String,
default: ''
default: 'Sign in with Google'
}
},
setup(props, { emit }) {
@@ -108,9 +108,6 @@
const toGmailLogin = ()=>{
message.info(t('account.canNotUtilize'))
}
const displayText = computed(() => {
return props.text || t('Login.LoginWithGoogle')
})
onBeforeUnmount(()=>{
var existingScript = document.querySelector(`script[src="${data.scriptSrc}"]`);
if(existingScript){
@@ -123,7 +120,6 @@
})
return {
toGmailLogin,
displayText,
}
},
})

View File

@@ -553,14 +553,13 @@ export default defineComponent({
loginType: "EMAIL",
userId: this.userId,
};
this.store.commit('set_loading', true)
this.$emit('update:isMask',true)
Https.axiosPost(Https.httpUrls.accountLogin, data)
.then((rv: any) => {
this.setSuccessLogin(rv);
this.store.commit('set_loading', false)
this.setSuccessLogin(rv);
})
.catch((res) => {
this.store.commit('set_loading', false)
this.$emit('update:isMask',false)
});
},1000)
},

View File

@@ -2,7 +2,7 @@
<div class="Container">
<div class="icon" @click="openWeiXinModel">
<img src="@/assets/images/loginPage/weiXinIcon.svg" alt="" />
<span>{{ displayText }}</span>
<span>{{ $props.text }}</span>
</div>
<weiXinModel ref="weiXinModel"></weiXinModel>
</div>
@@ -18,7 +18,6 @@ import {
toRefs,
} from "vue";
import weiXinModel from "./weiXinModel.vue";
import { useI18n } from 'vue-i18n'
export default defineComponent({
name: "login",
components: {
@@ -27,25 +26,20 @@ export default defineComponent({
props: {
text: {
type: String,
default: ''
default: "Sign in with Wechat",
},
},
setup(props) {
setup() {
let weiXinDom = reactive({
weiXinModel: null,
});
const { t } = useI18n()
const openWeiXinModel = () => {
weiXinDom.weiXinModel.init();
};
const displayText = computed(() => {
return props.text || t('Login.LoginWithWechat')
})
onMounted(() => {});
return {
...toRefs(weiXinDom),
openWeiXinModel,
displayText,
};
},
});

Some files were not shown because too many files have changed in this diff Show More