Compare commits
136 Commits
main
...
3fa7d407d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fa7d407d2 | |||
| 1fa60557df | |||
| 2b273ec70a | |||
| 45af83d0b2 | |||
| b4ea8907d7 | |||
| 7ed87f59ee | |||
|
|
567ae02c48 | ||
|
|
ae6f14efa9 | ||
| 048a548df8 | |||
| b9112a5606 | |||
|
|
8f1fea30ee | ||
|
|
29704f9b36 | ||
| 6cd54cda18 | |||
| 38c0b88abf | |||
|
|
e40b707501 | ||
|
|
6cb1e72798 | ||
|
|
11876f7fff | ||
|
|
59541a9d3d | ||
|
|
466d278b29 | ||
| 6fa5ade5b1 | |||
| c6b1efe719 | |||
| fa3063b3b5 | |||
| ebd5ceac41 | |||
| 9c9b219f3b | |||
|
|
73aca07391 | ||
|
|
780270882e | ||
|
|
f8e4ab8cdb | ||
|
|
bb53b6e486 | ||
|
|
e09c01cb7d | ||
|
|
2903553088 | ||
|
|
8c5105052d | ||
|
|
0e1e2cec39 | ||
|
|
086668d31b | ||
| d4da1b47ef | |||
| 1596b46ff1 | |||
| fd0ec4f7ff | |||
|
|
012c4036e0 | ||
| 688fb3daa0 | |||
|
|
5e5059ea73 | ||
|
|
386a103df1 | ||
| aad884d07c | |||
| f011300bef | |||
|
|
15f5c6b3a2 | ||
|
|
7fbd721512 | ||
| 271b8af4c4 | |||
| 81e230b79f | |||
|
|
7da3dcb0d7 | ||
| c6f3a44b81 | |||
| 0e16681404 | |||
|
|
b8f53e9f4a | ||
|
|
697dc36df4 | ||
|
|
5672307e33 | ||
|
|
fd80e2d3c7 | ||
| b160709f16 | |||
|
|
b52c96fa67 | ||
| 5a7e5e92a8 | |||
| 61dd9fb1c5 | |||
|
|
3d202e32c2 | ||
| 07b2334d61 | |||
| 892d96b904 | |||
| 41a42b1133 | |||
|
|
467ac9c24f | ||
|
|
1d4478e98e | ||
|
|
e24318e8ee | ||
|
|
5ad2e40221 | ||
|
|
c82afcbfd6 | ||
|
|
95d85572f3 | ||
|
|
727636e0f8 | ||
|
|
b01a375acc | ||
|
|
b3c396ba9c | ||
|
|
260db8e896 | ||
|
|
b5f393ceb7 | ||
| b1bea281ec | |||
| c9b65b6090 | |||
| 0ac6d6e93f | |||
| 743b3f0ef6 | |||
| 17f0045dbe | |||
|
|
1ae365b1f3 | ||
| b16c5c3263 | |||
| 8ed58d37d8 | |||
|
|
84175e94d1 | ||
|
|
ed83044f81 | ||
| 9cb6be3098 | |||
| 4ab4578081 | |||
|
|
652d89d3be | ||
|
|
17edeef461 | ||
|
|
aad6919ec3 | ||
|
|
baf161e695 | ||
|
|
51751f6b5e | ||
|
|
c1b051a185 | ||
| 1a5e285f09 | |||
| 911d1d8477 | |||
| e0261d4a37 | |||
| 50cb33ac43 | |||
| dcb63f88ae | |||
|
|
8dd9ddc93e | ||
|
|
2dc6bd1346 | ||
|
|
703d9cf781 | ||
| 261064bd23 | |||
| a1e8f3295e | |||
| a89c199ea8 | |||
| 5dc7514f05 | |||
| 925541ab99 | |||
| b800ca6b74 | |||
| 4a4afc4b10 | |||
| 26a55cea1d | |||
| 5e68456707 | |||
| e992aa0ecd | |||
|
|
69c32905e1 | ||
|
|
c582de3f60 | ||
|
|
0a8074eef8 | ||
| 4b90bd5928 | |||
| 4376c8c313 | |||
| 4746ff22a1 | |||
| 68f8a413bf | |||
| c10d05ead2 | |||
| de641d18d7 | |||
|
|
8a0beee181 | ||
|
|
3dcb6330e3 | ||
|
|
4bd8a54b34 | ||
|
|
8368c9382a | ||
| f2463da8cc | |||
| 6b8027f449 | |||
|
|
8c3fea8a24 | ||
|
|
819093db8c | ||
|
|
7dcfc3e705 | ||
|
|
7bb8b227b4 | ||
|
|
3d2fddbe7b | ||
|
|
9662610b1b | ||
|
|
56f958173b | ||
|
|
0e57e4de46 | ||
|
|
b0e365dcde | ||
|
|
5497f4fdbc | ||
|
|
3d6b622eef | ||
|
|
38ac7da504 | ||
|
|
1c895710d8 |
@@ -1,56 +0,0 @@
|
|||||||
name: AiDA WEB-Node.js Develop 分支构建部署123
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev_vite
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: "contains(github.event.head_commit.message, '[run build]')"
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [ 18.18.0 ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/DevelopVersion/develop-aida-web-front
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 1.检出代码
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 2.设置 Node.js 环境
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run build:dev
|
|
||||||
- run: ls -l
|
|
||||||
|
|
||||||
- name: 3.同步文件到远程服务器
|
|
||||||
uses: appleboy/scp-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
|
||||||
username: ${{ secrets.SERVER_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
source: "./dist/*"
|
|
||||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
|
||||||
ssh_options: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
||||||
strip_components: 0
|
|
||||||
|
|
||||||
- name: 4. 远程重载 Nginx 配置
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
|
||||||
username: ${{ secrets.SERVER_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
# 核心:执行 Nginx 重载命令
|
|
||||||
script: |
|
|
||||||
echo "尝试重载 Nginx 服务..."
|
|
||||||
# 💡 注意:执行此命令需要服务器用户具有 sudo 权限,并且配置了 NOPASSWD。
|
|
||||||
# 否则工作流可能会因为权限不足而失败。
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
echo "Nginx 重载命令已发送。"
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
name: 手动触发 AiDA WEB-Node.js Develop 分支构建部署
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [ 18.18.0 ]
|
|
||||||
env:
|
|
||||||
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/DevelopVersion/develop-aida-web-front
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 1.检出代码
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 2.设置 Node.js 环境
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run build:dev
|
|
||||||
- run: ls -l
|
|
||||||
|
|
||||||
- name: 3.同步文件到远程服务器
|
|
||||||
uses: appleboy/scp-action@v0.1.7
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
|
||||||
username: ${{ secrets.SERVER_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
source: "./dist/*"
|
|
||||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
|
||||||
ssh_options: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
||||||
strip_components: 0
|
|
||||||
|
|
||||||
- name: 4. 远程重载 Nginx 配置
|
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SERVER_HOST }}
|
|
||||||
username: ${{ secrets.SERVER_USER }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
# 核心:执行 Nginx 重载命令
|
|
||||||
script: |
|
|
||||||
echo "尝试重载 Nginx 服务..."
|
|
||||||
# 💡 注意:执行此命令需要服务器用户具有 sudo 权限,并且配置了 NOPASSWD。
|
|
||||||
# 否则工作流可能会因为权限不足而失败。
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
echo "Nginx 重载命令已发送。"
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
name: AiDA WEB-Node.js StableVersion 分支构建部署
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# cron为UTC时区,构建时间=部署时间-8小时 {*分 (-8)时 *日 *月 *周} ---
|
|
||||||
# 示例: 1月1日22点22分触发构建 cron写作 - '22 14 1 1 *'
|
|
||||||
- cron: '22 14 1 1 *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [ 18.18.0 ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 1.检出代码
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: StableVersion
|
|
||||||
|
|
||||||
- name: 2.打印当前分支信息
|
|
||||||
run: |
|
|
||||||
echo "Current branch being deployed is: $(git rev-parse --abbrev-ref HEAD)"
|
|
||||||
echo "The code is from the 'main' branch, as specified in 'actions/checkout'."
|
|
||||||
|
|
||||||
- name: 3.设置 Node.js 环境 ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
- run: npm install
|
|
||||||
- run: npm run build
|
|
||||||
- run: ls -l
|
|
||||||
|
|
||||||
- name: 3.5. 手动安装 AWS CLI v2 # 新增步骤:确保 aws 命令可用
|
|
||||||
run: |
|
|
||||||
echo "安装 AWS CLI V2..."
|
|
||||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
|
||||||
unzip awscliv2.zip
|
|
||||||
sudo ./aws/install --update
|
|
||||||
aws --version
|
|
||||||
echo "AWS CLI V2 安装完成。"
|
|
||||||
|
|
||||||
- name: 4.配置 AWS 凭证
|
|
||||||
uses: aws-actions/configure-aws-credentials@main
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
aws-region: 'ap-east-1'
|
|
||||||
|
|
||||||
- name: 5.同步 dist 目录到 S3
|
|
||||||
run: |
|
|
||||||
aws s3 sync dist/* s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
|
||||||
|
|
||||||
- name: 6.部署完成
|
|
||||||
run: echo "构建和部署到 S3 任务完成。"
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 5.同步 dist 目录到 S3
|
- name: 5.同步 dist 目录到 S3
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/* s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_NAME }}/ --acl public-read
|
||||||
|
|
||||||
- name: 6.部署完成
|
- name: 6.部署完成
|
||||||
run: echo "构建和部署到 S3 任务完成。"
|
run: echo "构建和部署到 S3 任务完成。"
|
||||||
BIN
public/image/toolsGuide/detailCN.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
public/image/toolsGuide/detailEN.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
23
src/App.vue
@@ -1,7 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
|
<div class="loading" v-show="loading"><a-spin :delay="0.5" /></div>
|
||||||
</template>
|
</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">
|
<style lang="less">
|
||||||
#app {
|
#app {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
@@ -9,7 +18,19 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
height: 100%;
|
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{
|
.ipad{
|
||||||
*{
|
*{
|
||||||
-webkit-touch-callout:none;
|
-webkit-touch-callout:none;
|
||||||
|
|||||||
@@ -54,6 +54,24 @@
|
|||||||
<div class="content unicode" style="display: block;">
|
<div class="content unicode" style="display: block;">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">混合模式</div>
|
||||||
|
<div class="code-name">&#xe7a4;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">更多</div>
|
||||||
|
<div class="code-name">&#xe60f;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">平铺</div>
|
||||||
|
<div class="code-name">&#xe8d7;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont"></span>
|
<span class="icon iconfont"></span>
|
||||||
<div class="name">裁剪</div>
|
<div class="name">裁剪</div>
|
||||||
@@ -276,9 +294,9 @@
|
|||||||
<pre><code class="language-css"
|
<pre><code class="language-css"
|
||||||
>@font-face {
|
>@font-face {
|
||||||
font-family: 'iconfont';
|
font-family: 'iconfont';
|
||||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||||
@@ -304,6 +322,33 @@
|
|||||||
<div class="content font-class">
|
<div class="content font-class">
|
||||||
<ul class="icon_lists dib-box">
|
<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">
|
<li class="dib">
|
||||||
<span class="icon iconfont icon-caijian"></span>
|
<span class="icon iconfont icon-caijian"></span>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -637,6 +682,30 @@
|
|||||||
<div class="content symbol">
|
<div class="content symbol">
|
||||||
<ul class="icon_lists dib-box">
|
<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">
|
<li class="dib">
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-caijian"></use>
|
<use xlink:href="#icon-caijian"></use>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4292253 */
|
font-family: "iconfont"; /* Project id 4292253 */
|
||||||
src: url('iconfont.woff2?t=1762934152017') format('woff2'),
|
src: url('iconfont.woff2?t=1766460927921') format('woff2'),
|
||||||
url('iconfont.woff?t=1762934152017') format('woff'),
|
url('iconfont.woff?t=1766460927921') format('woff'),
|
||||||
url('iconfont.ttf?t=1762934152017') format('truetype');
|
url('iconfont.ttf?t=1766460927921') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,18 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-hunhemoshi:before {
|
||||||
|
content: "\e7a4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gengduo:before {
|
||||||
|
content: "\e60f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-repeat:before {
|
||||||
|
content: "\e8d7";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-caijian:before {
|
.icon-caijian:before {
|
||||||
content: "\e650";
|
content: "\e650";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,27 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"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",
|
"icon_id": "22138606",
|
||||||
"name": "裁剪",
|
"name": "裁剪",
|
||||||
|
|||||||
BIN
src/assets/images/canvas/xiangao.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
src/assets/images/canvas/xiangaofenge.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/images/canvas/yinhua1.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 313 KiB |
9
src/assets/images/socialMediaLogo/biliBliIcon.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
10
src/assets/images/socialMediaLogo/faceBookIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2427_2565)">
|
||||||
|
<path d="M12 0C5.37264 0 0 5.37264 0 12C0 17.6275 3.87456 22.3498 9.10128 23.6467V15.6672H6.62688V12H9.10128V10.4198C9.10128 6.33552 10.9498 4.4424 14.9597 4.4424C15.72 4.4424 17.0318 4.59168 17.5685 4.74048V8.06448C17.2853 8.03472 16.7933 8.01984 16.1822 8.01984C14.2147 8.01984 13.4544 8.76528 13.4544 10.703V12H17.3741L16.7006 15.6672H13.4544V23.9122C19.3963 23.1946 24.0005 18.1354 24.0005 12C24 5.37264 18.6274 0 12 0Z" fill="#2C2C2C"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2427_2565">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 691 B |
|
Before Width: | Height: | Size: 370 KiB |
10
src/assets/images/socialMediaLogo/linkedinIcon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2427_2564)">
|
||||||
|
<path d="M22.2234 0H1.77187C0.792187 0 0 0.773438 0 1.72969V22.2656C0 23.2219 0.792187 24 1.77187 24H22.2234C23.2031 24 24 23.2219 24 22.2703V1.72969C24 0.773438 23.2031 0 22.2234 0ZM7.12031 20.4516H3.55781V8.99531H7.12031V20.4516ZM5.33906 7.43438C4.19531 7.43438 3.27188 6.51094 3.27188 5.37187C3.27188 4.23281 4.19531 3.30937 5.33906 3.30937C6.47813 3.30937 7.40156 4.23281 7.40156 5.37187C7.40156 6.50625 6.47813 7.43438 5.33906 7.43438ZM20.4516 20.4516H16.8937V14.8828C16.8937 13.5563 16.8703 11.8453 15.0422 11.8453C13.1906 11.8453 12.9094 13.2938 12.9094 14.7891V20.4516H9.35625V8.99531H12.7687V10.5609H12.8156C13.2891 9.66094 14.4516 8.70938 16.1813 8.70938C19.7859 8.70938 20.4516 11.0813 20.4516 14.1656V20.4516V20.4516Z" fill="#2C2C2C"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2427_2564">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 997 B |
10
src/assets/images/socialMediaLogo/socialIcons.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2426_2350)">
|
||||||
|
<path d="M23.5233 7.12823C23.5233 7.12823 23.2913 5.49009 22.5766 4.77079C21.6717 3.8241 20.6601 3.81946 20.196 3.76377C16.8733 3.52246 11.8846 3.52246 11.8846 3.52246H11.8754C11.8754 3.52246 6.88669 3.52246 3.564 3.76377C3.09994 3.81946 2.08828 3.8241 1.18336 4.77079C0.468703 5.49009 0.241312 7.12823 0.241312 7.12823C0.241312 7.12823 0 9.05409 0 10.9753V12.7759C0 14.6971 0.236672 16.6229 0.236672 16.6229C0.236672 16.6229 0.468703 18.2611 1.17872 18.9804C2.08364 19.9271 3.27164 19.8946 3.80067 19.9967C5.70333 20.1777 11.88 20.2334 11.88 20.2334C11.88 20.2334 16.8733 20.2241 20.196 19.9874C20.6601 19.9317 21.6717 19.9271 22.5766 18.9804C23.2913 18.2611 23.5233 16.6229 23.5233 16.6229C23.5233 16.6229 23.76 14.7017 23.76 12.7759V10.9753C23.76 9.05409 23.5233 7.12823 23.5233 7.12823ZM9.42511 14.9616V8.28374L15.8431 11.6343L9.42511 14.9616Z" fill="#2C2C2C"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2426_2350">
|
||||||
|
<rect width="23.76" height="23.76" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 68 KiB |
3
src/assets/images/socialMediaLogo/tikTokIcon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.9218 0H10.9175V16.1843C10.9175 18.1127 9.37747 19.6967 7.4609 19.6967C5.54433 19.6967 4.00424 18.1127 4.00424 16.1843C4.00424 14.2905 5.51011 12.7408 7.35825 12.672V8.60871C3.28553 8.67755 0 12.0177 0 16.1843C0 20.3854 3.35398 23.76 7.49514 23.76C11.6362 23.76 14.9902 20.351 14.9902 16.1843V7.88555C16.4961 8.98748 18.3442 9.64174 20.295 9.67619V5.61287C17.2833 5.50957 14.9218 3.03026 14.9218 0Z" fill="#2C2C2C"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 532 B |
|
Before Width: | Height: | Size: 191 KiB |
9
src/assets/images/socialMediaLogo/xiaoHongShuIcon.svg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 299 KiB |
@@ -1250,10 +1250,14 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
|||||||
background: #000 !important;
|
background: #000 !important;
|
||||||
border-color: #000 !important;
|
border-color: #000 !important;
|
||||||
}
|
}
|
||||||
|
.ant-spin .ant-spin-dot {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
.ant-spin-dot-item {
|
.ant-spin-dot-item {
|
||||||
background-color: #000000 !important;
|
background-color: #000000 !important;
|
||||||
width: 9px !important;
|
width: 0.9em !important;
|
||||||
height: 9px !important;
|
height: 0.9em !important;
|
||||||
}
|
}
|
||||||
.ant-spin {
|
.ant-spin {
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -1358,7 +1362,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
|||||||
}
|
}
|
||||||
.admin_page .admin_state_item > span {
|
.admin_page .admin_state_item > span {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 13rem;
|
min-width: 13rem;
|
||||||
}
|
}
|
||||||
.admin_page .admin_state_item > span > span {
|
.admin_page .admin_state_item > span > span {
|
||||||
color: red;
|
color: red;
|
||||||
|
|||||||
@@ -1378,10 +1378,14 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//loding样式
|
//loding样式
|
||||||
|
.ant-spin .ant-spin-dot{
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
.ant-spin-dot-item{
|
.ant-spin-dot-item{
|
||||||
background-color: #000000 !important;
|
background-color: #000000 !important;
|
||||||
width: 9px !important;
|
width: .9em !important;
|
||||||
height: 9px !important;
|
height: .9em !important;
|
||||||
}
|
}
|
||||||
.ant-spin{
|
.ant-spin{
|
||||||
color: #000;
|
color: #000;
|
||||||
@@ -1490,7 +1494,7 @@ tr > .ant-picker-cell-in-view.ant-picker-cell-range-hover-start:last-child::afte
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
>span{
|
>span{
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 13rem;
|
min-width: 13rem;
|
||||||
>span{
|
>span{
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
<div class="generalModel_btn">
|
<div class="generalModel_btn">
|
||||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
||||||
<svg
|
<svg
|
||||||
width="100%" height="100%"
|
width="100%"
|
||||||
|
height="100%"
|
||||||
viewBox="0 0 46 46"
|
viewBox="0 0 46 46"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -49,7 +50,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="allUserPoeration_center admin_page">
|
<div class="allUserPoeration_center admin_page">
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>{{ $t('admin.UserName') }}: <span>*</span></span>
|
<span>
|
||||||
|
{{ $t('admin.UserName') }}:
|
||||||
|
<span>*</span>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="userName"
|
v-model="userName"
|
||||||
:placeholder="$t('admin.enterUserName')"
|
:placeholder="$t('admin.enterUserName')"
|
||||||
@@ -58,7 +62,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>{{ $t('admin.UserEmail') }}: <span>*</span></span>
|
<span>
|
||||||
|
{{ $t('admin.UserEmail') }}:
|
||||||
|
<span>*</span>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
v-model="userEmail"
|
v-model="userEmail"
|
||||||
:placeholder="$t('admin.enterEmail')"
|
:placeholder="$t('admin.enterEmail')"
|
||||||
@@ -67,7 +74,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>{{ $t('admin.Password') }}: <span>*</span></span>
|
<span>
|
||||||
|
{{ $t('admin.Password') }}:
|
||||||
|
<span>*</span>
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
@focus="focus"
|
@focus="focus"
|
||||||
@blur="blur"
|
@blur="blur"
|
||||||
@@ -86,6 +96,19 @@
|
|||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div class="allUserPoeration_btn admin_page">
|
<div class="allUserPoeration_btn admin_page">
|
||||||
<div class="admin_search_item" @click="cancelDsign">{{ $t('admin.Close') }}</div>
|
<div class="admin_search_item" @click="cancelDsign">{{ $t('admin.Close') }}</div>
|
||||||
@@ -96,7 +119,7 @@
|
|||||||
<a-spin size="large" />
|
<a-spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
ref,
|
ref,
|
||||||
@@ -105,90 +128,114 @@ import {
|
|||||||
onMounted,
|
onMounted,
|
||||||
nextTick,
|
nextTick,
|
||||||
toRefs,
|
toRefs,
|
||||||
} from "vue";
|
computed
|
||||||
import { Https } from "@/tool/https";
|
} from 'vue'
|
||||||
import { Modal, message } from "ant-design-vue";
|
import { Https } from '@/tool/https'
|
||||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { formatTime, isEmail } from "@/tool/util";
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import md5 from "md5";
|
import { formatTime, isEmail } from '@/tool/util'
|
||||||
|
import md5 from 'md5'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {},
|
components: {},
|
||||||
emits: ["searchHistoryList"],
|
props: {
|
||||||
|
planOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['searchHistoryList'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const {t} = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { planOptions } = toRefs(props)
|
||||||
|
// 筛选出状态为 ACTIVE 的订阅计划
|
||||||
|
const activePlanOptions = computed(() => {
|
||||||
|
if (!planOptions.value || !Array.isArray(planOptions.value)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return planOptions.value.filter((plan: any) => plan.status === 'ACTIVE')
|
||||||
|
})
|
||||||
let operations = reactive({
|
let operations = reactive({
|
||||||
operationsModal: false,
|
operationsModal: false,
|
||||||
operationsEdit: false,
|
operationsEdit: false,
|
||||||
loadingShow: false,
|
loadingShow: false,
|
||||||
title: null,
|
title: null
|
||||||
});
|
})
|
||||||
let operationsData = reactive({
|
let operationsData = reactive({
|
||||||
accountId: -1,
|
accountId: -1,
|
||||||
userName: "",
|
userName: '',
|
||||||
userEmail: "",
|
userEmail: '',
|
||||||
password: "",
|
password: '',
|
||||||
oldPassword: "",
|
oldPassword: '',
|
||||||
credits: "",
|
credits: '',
|
||||||
});
|
subscriptionPlanId: '',
|
||||||
|
oldSubscriptionPlanId: ''
|
||||||
|
})
|
||||||
let state = ref([
|
let state = ref([
|
||||||
{
|
{
|
||||||
label: "visitor",
|
label: 'visitor',
|
||||||
value: "0",
|
value: '0'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "yearly",
|
label: 'yearly',
|
||||||
value: "1",
|
value: '1'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "monthly",
|
label: 'monthly',
|
||||||
value: "2",
|
value: '2'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "trial",
|
label: 'trial',
|
||||||
value: "3",
|
value: '3'
|
||||||
},
|
}
|
||||||
]);
|
])
|
||||||
let init = (funStr, data) => {
|
let init = (funStr, data) => {
|
||||||
operations.operationsModal = true;
|
operations.operationsModal = true
|
||||||
operations.operationsEdit = true;
|
operations.operationsEdit = true
|
||||||
operations.title = funStr;
|
operations.title = funStr
|
||||||
if (funStr.value == "Add") operations.operationsEdit = false;
|
if (funStr.value == 'Add') operations.operationsEdit = false
|
||||||
if (funStr.value == "Edit") {
|
if (funStr.value == 'Edit') {
|
||||||
operationsData.accountId = data.id;
|
operationsData.accountId = data.id
|
||||||
operationsData.userName = data.userName;
|
operationsData.userName = data.userName
|
||||||
operationsData.userEmail = data.userEmail;
|
operationsData.userEmail = data.userEmail
|
||||||
operationsData.password = data.userPassword?data.userPassword:null;
|
operationsData.password = data.userPassword ? data.userPassword : null
|
||||||
operationsData.oldPassword = data.userPassword;
|
operationsData.oldPassword = data.userPassword
|
||||||
// operationsData.validStartTime='2024-08-05T00:00:06'
|
// operationsData.validStartTime='2024-08-05T00:00:06'
|
||||||
// operationsData.validEndTime='2024-08-05T00:00:06'
|
// operationsData.validEndTime='2024-08-05T00:00:06'
|
||||||
operationsData.credits = data.creditsUsageLimit;
|
operationsData.credits = data.creditsUsageLimit
|
||||||
|
operationsData.subscriptionPlanId = data.subscriptionPlanId || ''
|
||||||
|
operationsData.oldSubscriptionPlanId = data.subscriptionPlanId || ''
|
||||||
// operationsData.accountId = data.accountId
|
// operationsData.accountId = data.accountId
|
||||||
// operationsData.userName = data.userName
|
// operationsData.userName = data.userName
|
||||||
// operationsData.userEmail = data.userEmail
|
// operationsData.userEmail = data.userEmail
|
||||||
// operationsData.validStartTime = formatTime(data.validStartTime)
|
// operationsData.validStartTime = formatTime(data.validStartTime)
|
||||||
// operationsData.validEndTime = formatTime(data.validEndTime)
|
// operationsData.validEndTime = formatTime(data.validEndTime)
|
||||||
}
|
}
|
||||||
};
|
if (funStr.value == 'Add') {
|
||||||
let focus = (event) => {
|
operationsData.subscriptionPlanId = ''
|
||||||
|
operationsData.oldSubscriptionPlanId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let focus = event => {
|
||||||
if (operationsData.password == operationsData.oldPassword) {
|
if (operationsData.password == operationsData.oldPassword) {
|
||||||
operationsData.password = "";
|
operationsData.password = ''
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
let blur = (event) => {
|
let blur = event => {
|
||||||
console.log(operationsData.password == "" && operationsData.oldPassword);
|
console.log(operationsData.password == '' && operationsData.oldPassword)
|
||||||
if (operationsData.password == "" && operationsData.oldPassword) {
|
if (operationsData.password == '' && operationsData.oldPassword) {
|
||||||
operationsData.password = operationsData.oldPassword;
|
operationsData.password = operationsData.oldPassword
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
let setAddData = () => {
|
let setAddData = () => {
|
||||||
return {
|
return {
|
||||||
creditsUsageLimit: operationsData.credits,
|
creditsUsageLimit: operationsData.credits,
|
||||||
userEmail: operationsData.userEmail,
|
userEmail: operationsData.userEmail,
|
||||||
userPassword: operationsData.password?md5(operationsData.password + "abc"):'',
|
userPassword: operationsData.password ? md5(operationsData.password + 'abc') : '',
|
||||||
userName: operationsData.userName,
|
userName: operationsData.userName,
|
||||||
};
|
subscriptionPlanId: operationsData.subscriptionPlanId
|
||||||
};
|
}
|
||||||
|
}
|
||||||
let setEditData = () => {
|
let setEditData = () => {
|
||||||
return {
|
return {
|
||||||
id: operationsData.accountId,
|
id: operationsData.accountId,
|
||||||
@@ -198,57 +245,63 @@ export default defineComponent({
|
|||||||
userPassword:
|
userPassword:
|
||||||
operationsData.password == operationsData.oldPassword
|
operationsData.password == operationsData.oldPassword
|
||||||
? null
|
? null
|
||||||
: md5(operationsData.password + "abc"),
|
: md5(operationsData.password + 'abc'),
|
||||||
};
|
subscriptionPlanId: operationsData.subscriptionPlanId
|
||||||
};
|
|
||||||
let cancelDsign = () => {
|
|
||||||
operationsData.accountId = -1;
|
|
||||||
operationsData.userName = "";
|
|
||||||
operationsData.userEmail = "";
|
|
||||||
operationsData.password = "";
|
|
||||||
operationsData.credits = "";
|
|
||||||
operations.operationsModal = false;
|
|
||||||
};
|
|
||||||
let setOk = () => {
|
|
||||||
let data;
|
|
||||||
if (operations.title?.value == "Add") {
|
|
||||||
data = setAddData();
|
|
||||||
if (!isEmail(data.userEmail)) {
|
|
||||||
message.info(t('admin.jsContent1'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!data.userName ||
|
|
||||||
!data.userEmail ||
|
|
||||||
!data.userPassword
|
|
||||||
)
|
|
||||||
return message.warning(t('admin.jsContent2'));
|
|
||||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(
|
|
||||||
(rv) => {
|
|
||||||
if (rv) {
|
|
||||||
cancelDsign();
|
|
||||||
emit("searchHistoryList");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
data = setEditData();
|
|
||||||
if (!isEmail(data.userEmail)) {
|
|
||||||
message.info("The email format is incorrect");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!data.userName || !data.userEmail)
|
|
||||||
return message.warning("Please check the input box marked with *");
|
|
||||||
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(
|
|
||||||
(rv) => {
|
|
||||||
if (rv) {
|
|
||||||
cancelDsign();
|
|
||||||
emit("searchHistoryList");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
let cancelDsign = () => {
|
||||||
|
operationsData.accountId = -1
|
||||||
|
operationsData.userName = ''
|
||||||
|
operationsData.userEmail = ''
|
||||||
|
operationsData.password = ''
|
||||||
|
operationsData.credits = ''
|
||||||
|
operationsData.subscriptionPlanId = ''
|
||||||
|
operationsData.oldSubscriptionPlanId = ''
|
||||||
|
operations.operationsModal = false
|
||||||
|
}
|
||||||
|
let setOk = () => {
|
||||||
|
let data
|
||||||
|
if (operations.title?.value == 'Add') {
|
||||||
|
data = setAddData()
|
||||||
|
if (!isEmail(data.userEmail)) {
|
||||||
|
message.info(t('admin.jsContent1'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!data.userName || !data.userEmail || !data.userPassword)
|
||||||
|
return message.warning(t('admin.jsContent2'))
|
||||||
|
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(rv => {
|
||||||
|
if (rv) {
|
||||||
|
cancelDsign()
|
||||||
|
emit('searchHistoryList')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data = setEditData()
|
||||||
|
if (!isEmail(data.userEmail)) {
|
||||||
|
message.info('The email format is incorrect')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!data.userName || !data.userEmail || !data.subscriptionPlanId)
|
||||||
|
return message.warning('Please check the input box marked with *')
|
||||||
|
const needSwitchPlan =
|
||||||
|
operationsData.subscriptionPlanId &&
|
||||||
|
operationsData.subscriptionPlanId !== operationsData.oldSubscriptionPlanId
|
||||||
|
Https.axiosPost(Https.httpUrls.addOrUpdateSubAccount, data).then(async rv => {
|
||||||
|
if (rv) {
|
||||||
|
if (needSwitchPlan) {
|
||||||
|
await Https.axiosGet(Https.httpUrls.switchSubAccountSubscribePlan, {
|
||||||
|
params: {
|
||||||
|
targetSubscriptionPlanId: operationsData.subscriptionPlanId,
|
||||||
|
subAccId: operationsData.accountId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cancelDsign()
|
||||||
|
emit('searchHistoryList')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...toRefs(operations),
|
...toRefs(operations),
|
||||||
...toRefs(operationsData),
|
...toRefs(operationsData),
|
||||||
@@ -258,14 +311,16 @@ export default defineComponent({
|
|||||||
focus,
|
focus,
|
||||||
blur,
|
blur,
|
||||||
setOk,
|
setOk,
|
||||||
};
|
planOptions,
|
||||||
|
activePlanOptions
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return {}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {},
|
||||||
methods: {},
|
methods: {}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.allUserPoeration_modal) {
|
:deep(.allUserPoeration_modal) {
|
||||||
|
|||||||
@@ -36,27 +36,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>{{ $t("admin.Email") }}:</span>
|
<span>{{ $t("admin.Email") }}:</span>
|
||||||
<input
|
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||||
v-model="email"
|
|
||||||
:placeholder="$t('admin.enterEmail')"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
type="text"
|
|
||||||
style="width: 250px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="admin_state_item">
|
|
||||||
<span>{{ $t("admin.UserName") }}:</span>
|
|
||||||
<a-select
|
|
||||||
v-model:value="ids"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 250px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
:placeholder="$t('admin.selectUserName')"
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="allUserList"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>Organization Name:</span>
|
<span>Organization Name:</span>
|
||||||
<input
|
<input
|
||||||
@@ -100,8 +82,9 @@
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { Https } from "@/tool/https";
|
import { Https } from "@/tool/https";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import SelectUser from "@/component/common/SelectUser.vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {},
|
components: { SelectUser },
|
||||||
setup() {
|
setup() {
|
||||||
const store: any = useStore();
|
const store: any = useStore();
|
||||||
let rangePickerValue: any = ref([]);
|
let rangePickerValue: any = ref([]);
|
||||||
@@ -176,9 +159,6 @@
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
let allUserList: any = computed(() => {
|
|
||||||
return store.state.adminPage.allUserList;
|
|
||||||
});
|
|
||||||
let ids = ref([]);
|
let ids = ref([]);
|
||||||
let email = ref("");
|
let email = ref("");
|
||||||
let dataList: any = ref([]);
|
let dataList: any = ref([]);
|
||||||
@@ -193,7 +173,6 @@
|
|||||||
rangeTimeValue,
|
rangeTimeValue,
|
||||||
columns,
|
columns,
|
||||||
dataList,
|
dataList,
|
||||||
allUserList,
|
|
||||||
ids,
|
ids,
|
||||||
email,
|
email,
|
||||||
renameData,
|
renameData,
|
||||||
@@ -251,7 +230,7 @@
|
|||||||
endTime: endDate,
|
endTime: endDate,
|
||||||
startTime: startDate,
|
startTime: startDate,
|
||||||
ids: ids,
|
ids: ids,
|
||||||
email: this.email.trim(),
|
email: this.email?.trim(),
|
||||||
organizationName: this.organizationName,
|
organizationName: this.organizationName,
|
||||||
};
|
};
|
||||||
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export default defineComponent({
|
|||||||
changeEvent:this.changeEvent,
|
changeEvent:this.changeEvent,
|
||||||
size:this.pageSize,
|
size:this.pageSize,
|
||||||
page:this.currentPage,
|
page:this.currentPage,
|
||||||
email:this.email.trim(),
|
email:this.email?.trim(),
|
||||||
}
|
}
|
||||||
Https.axiosPost(Https.httpUrls.getGenerateFrequency,data).then((rv: any) => {
|
Https.axiosPost(Https.httpUrls.getGenerateFrequency,data).then((rv: any) => {
|
||||||
if (rv) {
|
if (rv) {
|
||||||
|
|||||||
@@ -25,15 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>{{ $t('admin.UserName') }}:</span>
|
<span>{{ $t('admin.UserName') }}:</span>
|
||||||
<a-select
|
<SelectUser v-model="userIdList" labelKey="email" multiple />
|
||||||
v-model:value="userIdList"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 280px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
:placeholder="$t('admin.selectUserName')"
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="dataList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -78,16 +70,15 @@ import { LabelLayout } from 'echarts/features';
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import SelectUser from '@/component/common/SelectUser.vue'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
SelectUser
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
const store:any = useStore()
|
const store:any = useStore()
|
||||||
let filter:any = reactive({
|
let filter:any = reactive({
|
||||||
dataList:computed(()=>{
|
|
||||||
return store.state.adminPage.allUserList
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let filterData:any = reactive({
|
let filterData:any = reactive({
|
||||||
|
|||||||
@@ -32,27 +32,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>Email:</span>
|
<span>Email:</span>
|
||||||
<input
|
<!-- <input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
placeholder="Please enter email"
|
placeholder="Please enter email"
|
||||||
@keydown.enter="gettrialList"
|
@keydown.enter="gettrialList"
|
||||||
type="text"
|
type="text"
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
/>
|
/> -->
|
||||||
</div>
|
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||||
<div class="admin_state_item">
|
|
||||||
<span>User Name:</span>
|
|
||||||
<a-select
|
|
||||||
v-model:value="ids"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 250px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
placeholder="Select Item..."
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="allUserList"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>Organization Name:</span>
|
<span>Organization Name:</span>
|
||||||
<input
|
<input
|
||||||
@@ -95,8 +84,11 @@
|
|||||||
import { defineComponent, ref, createVNode, computed } from "vue";
|
import { defineComponent, ref, createVNode, computed } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { Https } from "@/tool/https";
|
import { Https } from "@/tool/https";
|
||||||
|
import SelectUser from "@/component/common/SelectUser.vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {},
|
components: {
|
||||||
|
SelectUser
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store: any = useStore();
|
const store: any = useStore();
|
||||||
let rangePickerValue: any = ref([]);
|
let rangePickerValue: any = ref([]);
|
||||||
@@ -238,9 +230,6 @@
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
let allUserList: any = computed(() => {
|
|
||||||
return store.state.adminPage.allUserList;
|
|
||||||
});
|
|
||||||
let ids = ref([]);
|
let ids = ref([]);
|
||||||
let email = ref("");
|
let email = ref("");
|
||||||
let dataList: any = ref([]);
|
let dataList: any = ref([]);
|
||||||
@@ -255,7 +244,6 @@
|
|||||||
rangeTimeValue,
|
rangeTimeValue,
|
||||||
columns,
|
columns,
|
||||||
dataList,
|
dataList,
|
||||||
allUserList,
|
|
||||||
ids,
|
ids,
|
||||||
email,
|
email,
|
||||||
renameData,
|
renameData,
|
||||||
@@ -312,7 +300,7 @@
|
|||||||
endTime: endDate,
|
endTime: endDate,
|
||||||
startTime: startDate,
|
startTime: startDate,
|
||||||
ids: ids,
|
ids: ids,
|
||||||
email: this.email.trim(),
|
email: this.email?.trim(),
|
||||||
organizationName: this.organizationName,
|
organizationName: this.organizationName,
|
||||||
};
|
};
|
||||||
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
Https.axiosGet(Https.httpUrls.getDesignStatistic, {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
:filter-option="filterOption"
|
:filter-option="filterOption"
|
||||||
placeholder="Select Item..."
|
placeholder="Select Item..."
|
||||||
max-tag-count="responsive"
|
max-tag-count="responsive"
|
||||||
:options="countryList"
|
:options="allCountry"
|
||||||
></a-select>
|
></a-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
@@ -192,9 +192,6 @@ export default defineComponent({
|
|||||||
cityList: computed(()=>{
|
cityList: computed(()=>{
|
||||||
return store.state.adminPage.city
|
return store.state.adminPage.city
|
||||||
}),
|
}),
|
||||||
countryList: computed(()=>{
|
|
||||||
return store.state.adminPage.country
|
|
||||||
}),
|
|
||||||
isAwayOrUnfold:false,
|
isAwayOrUnfold:false,
|
||||||
});
|
});
|
||||||
let filterData: any = reactive({
|
let filterData: any = reactive({
|
||||||
@@ -471,9 +468,10 @@ export default defineComponent({
|
|||||||
filter.dataList = rv.content;
|
filter.dataList = rv.content;
|
||||||
filterData.total = rv.total;
|
filterData.total = rv.total;
|
||||||
filter.tableLoading = false;
|
filter.tableLoading = false;
|
||||||
rv.content.forEach((item: any) => {
|
filterData.totalPayer = rv.content.reduce((total: number, item: any) => {
|
||||||
filterData.totalPayer += Number(item.payerTotal)
|
const value = item && item.status === 'Success' ? parseFloat(item.payerTotal) : 0;
|
||||||
})
|
return total + (isNaN(value) ? 0 : value);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// this.workspaceItem.position = this.singleTypeList[0].label
|
// this.workspaceItem.position = this.singleTypeList[0].label
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,9 +139,6 @@ export default defineComponent({
|
|||||||
let filter: any = reactive({
|
let filter: any = reactive({
|
||||||
dataList: [],
|
dataList: [],
|
||||||
tableLoading: false,
|
tableLoading: false,
|
||||||
allUserList: computed(()=>{
|
|
||||||
return store.state.adminPage.allUserList
|
|
||||||
}),
|
|
||||||
rowSelection:computed(() => {
|
rowSelection:computed(() => {
|
||||||
return {
|
return {
|
||||||
selectedRowKeys: unref(selectedRowKeys),
|
selectedRowKeys: unref(selectedRowKeys),
|
||||||
|
|||||||
@@ -40,27 +40,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>Email:</span>
|
<span>Email:</span>
|
||||||
<input
|
<!-- <input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
placeholder="Please enter email"
|
placeholder="Please enter email"
|
||||||
@keydown.enter="gettrialList"
|
@keydown.enter="gettrialList"
|
||||||
type="text"
|
type="text"
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
/>
|
/> -->
|
||||||
</div>
|
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||||
<div class="admin_state_item">
|
|
||||||
<span>User Name:</span>
|
|
||||||
<a-select
|
|
||||||
v-model:value="ids"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 250px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
placeholder="Select Item..."
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="allUserList"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>User Type:</span>
|
<span>User Type:</span>
|
||||||
<a-select
|
<a-select
|
||||||
@@ -160,16 +149,14 @@ import { formatTime } from "@/tool/util";
|
|||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { Https } from "@/tool/https";
|
import { Https } from "@/tool/https";
|
||||||
import allUserPoerationsVue from "./allUserPoerations.vue";
|
import allUserPoerationsVue from "./allUserPoerations.vue";
|
||||||
|
import SelectUser from '@/component/common/SelectUser.vue'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {allUserPoerationsVue,},
|
components: {allUserPoerationsVue,SelectUser},
|
||||||
setup() {
|
setup() {
|
||||||
const store:any = useStore()
|
const store:any = useStore()
|
||||||
let filter: any = reactive({
|
let filter: any = reactive({
|
||||||
dataList: [],
|
dataList: [],
|
||||||
tableLoading: false,
|
tableLoading: false,
|
||||||
allUserList: computed(()=>{
|
|
||||||
return store.state.adminPage.allUserList
|
|
||||||
}),
|
|
||||||
allCountry:[],
|
allCountry:[],
|
||||||
isAwayOrUnfold:false
|
isAwayOrUnfold:false
|
||||||
});
|
});
|
||||||
@@ -436,7 +423,7 @@ export default defineComponent({
|
|||||||
page: filterData.currentPage,
|
page: filterData.currentPage,
|
||||||
systemUser: filterData.systemUser,
|
systemUser: filterData.systemUser,
|
||||||
country: filterData.country,
|
country: filterData.country,
|
||||||
email: filterData.email.trim(),
|
email: filterData.email?.trim(),
|
||||||
userType: filterData.userType,
|
userType: filterData.userType,
|
||||||
ids: filterData.ids,
|
ids: filterData.ids,
|
||||||
occupation: filterData.occupation,
|
occupation: filterData.occupation,
|
||||||
|
|||||||
@@ -1,352 +1,392 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="allUserPoerationModal" ref="allUserPoerationModal"></div>
|
<div class="allUserPoerationModal" ref="allUserPoerationModal"></div>
|
||||||
<a-modal
|
<a-modal
|
||||||
class="allUserPoeration_modal generalModel"
|
class="allUserPoeration_modal generalModel"
|
||||||
v-model:visible="operationsModal"
|
v-model:visible="operationsModal"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
:get-container="() => $refs.allUserPoerationModal"
|
:get-container="() => $refs.allUserPoerationModal"
|
||||||
width="50%"
|
width="50%"
|
||||||
:maskClosable="false"
|
:maskClosable="false"
|
||||||
:centered="true"
|
:centered="true"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:mask="true"
|
:mask="true"
|
||||||
wrapClassName="#app"
|
wrapClassName="#app"
|
||||||
:keyboard="false"
|
:keyboard="false"
|
||||||
>
|
>
|
||||||
<div class="generalModel_btn">
|
<div class="generalModel_btn">
|
||||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
||||||
<svg width="100%" height="100%" viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3"/>
|
width="100%"
|
||||||
<rect x="32.5063" y="12" width="3" height="29" rx="1.5" transform="rotate(45 32.5063 12)" fill="white"/>
|
height="100%"
|
||||||
<rect x="34.6274" y="32.5059" width="3" height="29" rx="1.5" transform="rotate(135 34.6274 32.5059)" fill="white"/>
|
viewBox="0 0 46 46"
|
||||||
</svg>
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
>
|
||||||
</div>
|
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3" />
|
||||||
<div class="modal_title_text">
|
<rect
|
||||||
<div>{{ title }} User</div>
|
x="32.5063"
|
||||||
</div>
|
y="12"
|
||||||
<div class="allUserPoeration_center admin_page">
|
width="3"
|
||||||
<div class="admin_state_item">
|
height="29"
|
||||||
<span>User Name: <span>*</span></span>
|
rx="1.5"
|
||||||
<input
|
transform="rotate(45 32.5063 12)"
|
||||||
:disabled="title != 'Add'"
|
fill="white"
|
||||||
:class="{active:title != 'Add'}"
|
/>
|
||||||
v-model="userName"
|
<rect
|
||||||
placeholder="Please enter user name"
|
x="34.6274"
|
||||||
type="text"
|
y="32.5059"
|
||||||
style="width: 250px"
|
width="3"
|
||||||
/>
|
height="29"
|
||||||
</div>
|
rx="1.5"
|
||||||
<div class="admin_state_item">
|
transform="rotate(135 34.6274 32.5059)"
|
||||||
<span>User Email: <span>*</span></span>
|
fill="white"
|
||||||
<input
|
/>
|
||||||
:disabled="title != 'Add'"
|
</svg>
|
||||||
:class="{active:title != 'Add'}"
|
</div>
|
||||||
v-model="userEmail"
|
</div>
|
||||||
placeholder="Please enter email"
|
<div class="modal_title_text">
|
||||||
type="text"
|
<div>{{ title }} User</div>
|
||||||
style="width: 250px"
|
</div>
|
||||||
/>
|
<div class="allUserPoeration_center admin_page">
|
||||||
</div>
|
<div class="admin_state_item">
|
||||||
<div class="admin_state_item">
|
<span>
|
||||||
<span>Create Time: <span>*</span></span>
|
User Name:
|
||||||
<a-date-picker :disabled="title != 'Add'" style="width: 250px" valueFormat="YYYY-MM-DDTHH:mm:ss" class="range_picker" show-time placeholder="Create Time" v-model:value="validStartTime">
|
<span>*</span>
|
||||||
<template #suffixIcon>
|
</span>
|
||||||
<span
|
<input
|
||||||
class="icon iconfont range_picker_icon icon-rili"
|
:disabled="title != 'Add'"
|
||||||
></span>
|
:class="{ active: title != 'Add' }"
|
||||||
</template>
|
v-model="userName"
|
||||||
</a-date-picker>
|
placeholder="Please enter user name"
|
||||||
</div>
|
type="text"
|
||||||
<div class="admin_state_item">
|
style="width: 250px"
|
||||||
<span>End Time: <span>*</span></span>
|
/>
|
||||||
<a-date-picker style="width: 250px" valueFormat="YYYY-MM-DDTHH:mm:ss" class="range_picker" show-time placeholder="End Time" v-model:value="validEndTime">
|
</div>
|
||||||
<template #suffixIcon>
|
<div class="admin_state_item">
|
||||||
<span
|
<span>
|
||||||
class="icon iconfont range_picker_icon icon-rili"
|
User Email:
|
||||||
></span>
|
<span>*</span>
|
||||||
</template>
|
</span>
|
||||||
</a-date-picker>
|
<input
|
||||||
</div>
|
:disabled="title != 'Add'"
|
||||||
<div class="admin_state_item">
|
:class="{ active: title != 'Add' }"
|
||||||
<span>User Type:<span>*</span></span>
|
v-model="userEmail"
|
||||||
<a-select
|
placeholder="Please enter email"
|
||||||
v-model:value="systemUser"
|
type="text"
|
||||||
size="large"
|
style="width: 250px"
|
||||||
style="width: 250px"
|
/>
|
||||||
optionFilterProp="label"
|
</div>
|
||||||
:options="state"
|
<div class="admin_state_item">
|
||||||
placeholder="Please select"
|
<span>
|
||||||
allowClear
|
Create Time:
|
||||||
show-search
|
<span>*</span>
|
||||||
></a-select>
|
</span>
|
||||||
</div>
|
<a-date-picker
|
||||||
<div class="admin_state_item">
|
:disabled="title != 'Add'"
|
||||||
<span>Credits:</span>
|
style="width: 250px"
|
||||||
<input
|
valueFormat="YYYY-MM-DDTHH:mm:ss"
|
||||||
v-model="credits"
|
class="range_picker"
|
||||||
placeholder="Please enter credits"
|
show-time
|
||||||
type="text"
|
placeholder="Create Time"
|
||||||
style="width: 250px"
|
v-model:value="validStartTime"
|
||||||
/>
|
>
|
||||||
</div>
|
<template #suffixIcon>
|
||||||
<div class="admin_state_item">
|
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
||||||
<span>Country or Region:</span>
|
</template>
|
||||||
<input
|
</a-date-picker>
|
||||||
|
</div>
|
||||||
|
<div class="admin_state_item">
|
||||||
|
<span>
|
||||||
|
End Time:
|
||||||
|
<span>*</span>
|
||||||
|
</span>
|
||||||
|
<a-date-picker
|
||||||
|
style="width: 250px"
|
||||||
|
valueFormat="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
class="range_picker"
|
||||||
|
show-time
|
||||||
|
placeholder="End Time"
|
||||||
|
v-model:value="validEndTime"
|
||||||
|
>
|
||||||
|
<template #suffixIcon>
|
||||||
|
<span class="icon iconfont range_picker_icon icon-rili"></span>
|
||||||
|
</template>
|
||||||
|
</a-date-picker>
|
||||||
|
</div>
|
||||||
|
<div class="admin_state_item">
|
||||||
|
<span>
|
||||||
|
User Type:
|
||||||
|
<span>*</span>
|
||||||
|
</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="systemUser"
|
||||||
|
size="large"
|
||||||
|
style="width: 250px"
|
||||||
|
optionFilterProp="label"
|
||||||
|
:options="state"
|
||||||
|
placeholder="Please select"
|
||||||
|
allowClear
|
||||||
|
show-search
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div class="admin_state_item">
|
||||||
|
<span>Credits:</span>
|
||||||
|
<input
|
||||||
|
v-model="credits"
|
||||||
|
placeholder="Please enter credits"
|
||||||
|
type="text"
|
||||||
|
style="width: 250px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="admin_state_item">
|
||||||
|
<span>Country or Region:</span>
|
||||||
|
<!-- <input
|
||||||
:disabled="title != 'Add'"
|
:disabled="title != 'Add'"
|
||||||
:class="{active:title != 'Add'}"
|
:class="{active:title != 'Add'}"
|
||||||
v-model="country"
|
v-model="country"
|
||||||
placeholder="Please enter country"
|
placeholder="Please enter country"
|
||||||
type="text"
|
type="text"
|
||||||
style="width: 250px"
|
style="width: 250px"
|
||||||
/>
|
/> -->
|
||||||
</div>
|
<a-select
|
||||||
<div class="admin_state_item">
|
v-model:value="country"
|
||||||
<span>Organization Name:</span>
|
:disabled="title != 'Add'"
|
||||||
<input
|
:class="{ active: title != 'Add' }"
|
||||||
:disabled="title != 'Add'"
|
:allowClear="true"
|
||||||
:class="{active:title != 'Add'}"
|
show-search
|
||||||
v-model="organizationName"
|
style="width: 250px"
|
||||||
placeholder="Please enter Organization Name"
|
:filter-option="filterOption"
|
||||||
type="text"
|
placeholder="Select Country or Region"
|
||||||
style="width: 250px"
|
max-tag-count="responsive"
|
||||||
/>
|
:options="allCountry"
|
||||||
</div>
|
/>
|
||||||
<div class="admin_state_item">
|
</div>
|
||||||
<span>Sub Account Num:</span>
|
</div>
|
||||||
<input
|
<div class="allUserPoeration_btn admin_page">
|
||||||
:disabled="title != 'Add'"
|
<div class="admin_search_item" @click="cancelDsign">Close</div>
|
||||||
:class="{active:title != 'Add'}"
|
<div class="admin_search_item" @click="setOk">OK</div>
|
||||||
v-model="subAccountNum"
|
</div>
|
||||||
placeholder="Please enter Sub Account Num"
|
</a-modal>
|
||||||
type="number"
|
<div class="mark_loading" v-show="loadingShow">
|
||||||
style="width: 250px"
|
<a-spin size="large" />
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="allUserPoeration_btn admin_page">
|
|
||||||
<div class="admin_search_item" @click="cancelDsign">
|
|
||||||
Close
|
|
||||||
</div>
|
|
||||||
<div class="admin_search_item" @click="setOk">
|
|
||||||
OK
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-modal>
|
|
||||||
<div class="mark_loading" v-show="loadingShow">
|
|
||||||
<a-spin size="large" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent, ref, reactive, watch, onMounted, nextTick, toRefs } from "vue";
|
import { defineComponent, ref, reactive, watch, onMounted, nextTick, toRefs } from 'vue'
|
||||||
import { Https } from "@/tool/https";
|
import { Https } from '@/tool/https'
|
||||||
import { Modal, message } from "ant-design-vue";
|
import { Modal, message } from 'ant-design-vue'
|
||||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { formatTime } from "@/tool/util";
|
import { formatTime } from '@/tool/util'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {},
|
||||||
},
|
emits: ['searchHistoryList'],
|
||||||
emits: ['searchHistoryList'],
|
setup(props, { emit }) {
|
||||||
setup(props,{emit}) {
|
let operations = reactive({
|
||||||
let operations = reactive({
|
operationsModal: false,
|
||||||
operationsModal:false,
|
operationsEdit: false,
|
||||||
operationsEdit:false,
|
loadingShow: false,
|
||||||
loadingShow:false,
|
title: ''
|
||||||
title:''
|
})
|
||||||
})
|
let operationsData = reactive({
|
||||||
let operationsData = reactive({
|
accountId: -1,
|
||||||
accountId:-1,
|
userName: '',
|
||||||
userName:'',
|
userEmail: '',
|
||||||
userEmail:'',
|
validStartTime: '',
|
||||||
validStartTime:'',
|
validEndTime: '',
|
||||||
validEndTime:'',
|
systemUser: '',
|
||||||
systemUser:'',
|
credits: '',
|
||||||
credits:'',
|
country: ''
|
||||||
country:'',
|
})
|
||||||
organizationName:'',
|
let state = ref([
|
||||||
subAccountNum:0,
|
{
|
||||||
})
|
label: 'visitor',
|
||||||
let state = ref([
|
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',
|
label: 'userInEvent',
|
||||||
},
|
value: '4'
|
||||||
{
|
},
|
||||||
label: "userInEvent",
|
{
|
||||||
value: "4",
|
label: 'Edu Admin',
|
||||||
},
|
value: '7'
|
||||||
{
|
}
|
||||||
label: "Edu Admin",
|
])
|
||||||
value: "7",
|
let init = (funStr, data) => {
|
||||||
},
|
operations.operationsModal = true
|
||||||
]);
|
operations.operationsEdit = true
|
||||||
let init = (funStr,data)=>{
|
operations.title = funStr
|
||||||
operations.operationsModal = true
|
if (funStr == 'Add') operations.operationsEdit = false
|
||||||
operations.operationsEdit = true
|
if (funStr == 'Edit') {
|
||||||
operations.title = funStr
|
operationsData.organizationName = data.organizationName
|
||||||
if(funStr == 'Add') operations.operationsEdit = false
|
operationsData.subAccountNum = data.subAccountNum ? data.subAccountNum : 0
|
||||||
if(funStr == 'Edit'){
|
let startTime = data.validStartTime
|
||||||
operationsData.organizationName = data.organizationName
|
? formatTime(data.validStartTime / 1000, 'YYYY-MM-DDThh:mm:ss')
|
||||||
operationsData.subAccountNum = data.subAccountNum?data.subAccountNum:0
|
: ''
|
||||||
let startTime = data.validStartTime?formatTime(data.validStartTime / 1000,"YYYY-MM-DDThh:mm:ss"):''
|
let endTime = data.validEndTime
|
||||||
let endTime = data.validEndTime?formatTime(data.validEndTime / 1000,"YYYY-MM-DDThh:mm:ss"):''
|
? formatTime(data.validEndTime / 1000, 'YYYY-MM-DDThh:mm:ss')
|
||||||
operationsData.accountId=data.id
|
: ''
|
||||||
operationsData.userName=data.userName
|
operationsData.accountId = data.id
|
||||||
operationsData.userEmail=data.userEmail
|
operationsData.userName = data.userName
|
||||||
// operationsData.validStartTime='2024-08-05T00:00:06'
|
operationsData.userEmail = data.userEmail
|
||||||
// operationsData.validEndTime='2024-08-05T00:00:06'
|
// operationsData.validStartTime='2024-08-05T00:00:06'
|
||||||
operationsData.validStartTime=startTime
|
// operationsData.validEndTime='2024-08-05T00:00:06'
|
||||||
operationsData.validEndTime=endTime
|
operationsData.validStartTime = startTime
|
||||||
operationsData.systemUser=String(data.systemUser)
|
operationsData.validEndTime = endTime
|
||||||
operationsData.credits=data.credits
|
operationsData.systemUser = String(data.systemUser)
|
||||||
operationsData.country=data.country
|
operationsData.credits = data.credits
|
||||||
// operationsData.accountId = data.accountId
|
operationsData.country = data.country
|
||||||
// operationsData.userName = data.userName
|
// operationsData.accountId = data.accountId
|
||||||
// operationsData.userEmail = data.userEmail
|
// operationsData.userName = data.userName
|
||||||
// operationsData.validStartTime = formatTime(data.validStartTime)
|
// operationsData.userEmail = data.userEmail
|
||||||
// operationsData.validEndTime = formatTime(data.validEndTime)
|
// operationsData.validStartTime = formatTime(data.validStartTime)
|
||||||
}
|
// operationsData.validEndTime = formatTime(data.validEndTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let setTime = time => {
|
||||||
|
if (time) {
|
||||||
|
const date = new Date(time)
|
||||||
|
const timestamp = date.getTime() // 转换为秒数
|
||||||
|
return timestamp
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let setAddData = () => {
|
||||||
|
return {
|
||||||
|
country: operationsData.country,
|
||||||
|
credits: operationsData.credits,
|
||||||
|
systemUser: operationsData.systemUser,
|
||||||
|
userEmail: operationsData.userEmail,
|
||||||
|
userName: operationsData.userName,
|
||||||
|
validEndTime: setTime(operationsData.validEndTime),
|
||||||
|
validStartTime: setTime(operationsData.validStartTime),
|
||||||
|
organizationName: operationsData.organizationName
|
||||||
|
? operationsData.organizationName
|
||||||
|
: null,
|
||||||
|
subAccountNum: operationsData.subAccountNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let setEditData = () => {
|
||||||
|
return {
|
||||||
|
accountId: operationsData.accountId,
|
||||||
|
credits: operationsData.credits,
|
||||||
|
systemUser: operationsData.systemUser,
|
||||||
|
validEndTime: setTime(operationsData.validEndTime),
|
||||||
|
userName: operationsData.userName,
|
||||||
|
userEmail: operationsData.userEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cancelDsign = () => {
|
||||||
|
operationsData.accountId = -1
|
||||||
|
operationsData.userName = ''
|
||||||
|
operationsData.userEmail = ''
|
||||||
|
operationsData.validStartTime = ''
|
||||||
|
operationsData.validEndTime = ''
|
||||||
|
operationsData.systemUser = ''
|
||||||
|
operationsData.credits = ''
|
||||||
|
operationsData.country = ''
|
||||||
|
operations.operationsModal = false
|
||||||
|
}
|
||||||
|
let setOk = () => {
|
||||||
|
let data
|
||||||
|
if (operations.title == 'Add') {
|
||||||
|
data = setAddData()
|
||||||
|
if (
|
||||||
|
!data.userName ||
|
||||||
|
!data.userEmail ||
|
||||||
|
!data.validStartTime ||
|
||||||
|
!data.validEndTime ||
|
||||||
|
!data.systemUser
|
||||||
|
)
|
||||||
|
return message.warning('Please check the input box marked with *')
|
||||||
|
Https.axiosPost(Https.httpUrls.adminAddUser, data).then(rv => {
|
||||||
|
if (rv) {
|
||||||
|
cancelDsign()
|
||||||
|
emit('searchHistoryList')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data = setEditData()
|
||||||
|
if (!data.userName || !data.userEmail || !data.validEndTime || !data.systemUser)
|
||||||
|
return message.warning('Please check the input box marked with *')
|
||||||
|
Https.axiosPost(Https.httpUrls.modifyUser, {}, { params: data }).then(rv => {
|
||||||
|
if (rv) {
|
||||||
|
cancelDsign()
|
||||||
|
emit('searchHistoryList')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
const allCountry = ref([])
|
||||||
let setTime = (time) =>{
|
const filterOption = (input, option) => {
|
||||||
if(time){
|
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
const date = new Date(time);
|
}
|
||||||
const timestamp = date.getTime(); // 转换为秒数
|
onMounted(() => {
|
||||||
return timestamp
|
const countryList = sessionStorage.getItem('allCountry')
|
||||||
}else{
|
if (countryList) {
|
||||||
return ''
|
allCountry.value = JSON.parse(countryList)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
return {
|
||||||
let setAddData = ()=>{
|
...toRefs(operations),
|
||||||
return {
|
...toRefs(operationsData),
|
||||||
"country": operationsData.country,
|
state,
|
||||||
"credits": operationsData.credits,
|
cancelDsign,
|
||||||
"systemUser": operationsData.systemUser,
|
init,
|
||||||
"userEmail": operationsData.userEmail,
|
setOk,
|
||||||
"userName": operationsData.userName,
|
allCountry,
|
||||||
"validEndTime": setTime(operationsData.validEndTime),
|
filterOption
|
||||||
"validStartTime": setTime(operationsData.validStartTime),
|
}
|
||||||
"organizationName": operationsData.organizationName?operationsData.organizationName:null,
|
}
|
||||||
"subAccountNum": operationsData.subAccountNum,
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
let setEditData = ()=>{
|
|
||||||
return {
|
|
||||||
"accountId": operationsData.accountId,
|
|
||||||
"credits": operationsData.credits,
|
|
||||||
"systemUser": operationsData.systemUser,
|
|
||||||
"validEndTime": setTime(operationsData.validEndTime),
|
|
||||||
"userName": operationsData.userName,
|
|
||||||
"userEmail": operationsData.userEmail,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let cancelDsign = ()=>{
|
|
||||||
operationsData.accountId=-1
|
|
||||||
operationsData.userName=''
|
|
||||||
operationsData.userEmail=''
|
|
||||||
operationsData.validStartTime=''
|
|
||||||
operationsData.validEndTime=''
|
|
||||||
operationsData.systemUser=''
|
|
||||||
operationsData.credits=''
|
|
||||||
operationsData.country=''
|
|
||||||
operations.operationsModal = false
|
|
||||||
}
|
|
||||||
let setOk = ()=>{
|
|
||||||
let data
|
|
||||||
if(operations.title == 'Add'){
|
|
||||||
data = setAddData()
|
|
||||||
if(!data.userName || !data.userEmail || !data.validStartTime || !data.validEndTime || !data.systemUser)return message.warning('Please check the input box marked with *')
|
|
||||||
Https.axiosPost(Https.httpUrls.adminAddUser, data).then(
|
|
||||||
(rv) => {
|
|
||||||
if (rv) {
|
|
||||||
cancelDsign()
|
|
||||||
emit('searchHistoryList')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
data = setEditData()
|
|
||||||
if(!data.userName || !data.userEmail || !data.validEndTime || !data.systemUser)return message.warning('Please check the input box marked with *')
|
|
||||||
Https.axiosPost(Https.httpUrls.modifyUser,{},{params:data}).then(
|
|
||||||
(rv) => {
|
|
||||||
if (rv) {
|
|
||||||
cancelDsign()
|
|
||||||
emit('searchHistoryList')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...toRefs(operations),
|
|
||||||
...toRefs(operationsData),
|
|
||||||
state,
|
|
||||||
cancelDsign,
|
|
||||||
init,
|
|
||||||
setOk,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {},
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
:deep(.allUserPoeration_modal){
|
:deep(.allUserPoeration_modal) {
|
||||||
.ant-modal-body{
|
.ant-modal-body {
|
||||||
height: auto;
|
height: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
||||||
.allUserPoeration_modal {
|
.allUserPoeration_modal {
|
||||||
.closeIcon {
|
.closeIcon {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
}
|
||||||
|
> .admin_state_item {
|
||||||
|
> span {
|
||||||
|
width: 15rem;
|
||||||
}
|
}
|
||||||
> .admin_state_item{
|
}
|
||||||
> span{
|
.allUserPoeration_btn {
|
||||||
width: 15rem;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
}
|
height: auto;
|
||||||
.allUserPoeration_btn{
|
justify-content: flex-end;
|
||||||
display: flex;
|
padding: 1rem 0;
|
||||||
flex-direction: row;
|
.admin_search_item {
|
||||||
height: auto;
|
margin-bottom: 0;
|
||||||
justify-content: flex-end;
|
}
|
||||||
padding: 1rem 0;
|
}
|
||||||
.admin_search_item{
|
.allUserPoeration_center {
|
||||||
margin-bottom: 0;
|
flex: 1;
|
||||||
}
|
overflow-y: auto;
|
||||||
}
|
flex-direction: row;
|
||||||
.allUserPoeration_center{
|
flex-wrap: wrap;
|
||||||
flex: 1;
|
}
|
||||||
overflow-y: auto;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -25,15 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>User:</span>
|
<span>User:</span>
|
||||||
<a-select
|
<SelectUser v-model="userIdList" labelKey="email" multiple />
|
||||||
v-model:value="userIdList"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 280px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
placeholder="Select Item..."
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="dataList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -62,15 +54,14 @@ import { PieChart } from 'echarts/charts';
|
|||||||
import { LabelLayout } from 'echarts/features';
|
import { LabelLayout } from 'echarts/features';
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import SelectUser from '@/component/common/SelectUser.vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
SelectUser
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store:any = useStore()
|
const store:any = useStore()
|
||||||
let filter:any = reactive({
|
let filter:any = reactive({
|
||||||
dataList:computed(()=>{
|
|
||||||
return store.state.adminPage.allUserList
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let filterData:any = reactive({
|
let filterData:any = reactive({
|
||||||
|
|||||||
1133
src/component/Administrator/subscriptionPlan.vue
Normal file
@@ -37,26 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin_state_item">
|
<div class="admin_state_item">
|
||||||
<span>Email:</span>
|
<span>Email:</span>
|
||||||
<input
|
<SelectUser v-model="email" labelKey="email" valueKey="email" />
|
||||||
v-model="email"
|
|
||||||
placeholder="Please enter email"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
type="text"
|
|
||||||
style="width: 250px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="admin_state_item">
|
|
||||||
<span>User Name:</span>
|
|
||||||
<a-select
|
|
||||||
v-model:value="ids"
|
|
||||||
mode="multiple"
|
|
||||||
style="width: 250px"
|
|
||||||
:filter-option="filterOption"
|
|
||||||
placeholder="Select Item..."
|
|
||||||
max-tag-count="responsive"
|
|
||||||
:options="allUserList"
|
|
||||||
@keydown.enter="gettrialList"
|
|
||||||
></a-select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin_search">
|
<div class="admin_search">
|
||||||
@@ -96,17 +77,17 @@ import { defineComponent, ref, createVNode, computed, reactive, toRefs, onMounte
|
|||||||
import { formatTime } from "@/tool/util";
|
import { formatTime } from "@/tool/util";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { Https } from "@/tool/https";
|
import { Https } from "@/tool/https";
|
||||||
|
import SelectUser from '@/component/common/SelectUser.vue'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
SelectUser
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store:any = useStore()
|
const store:any = useStore()
|
||||||
let filter:any = reactive({
|
let filter:any = reactive({
|
||||||
dataList:[],
|
dataList:[],
|
||||||
tableLoading:false,
|
tableLoading:false,
|
||||||
allUserList: computed(()=>{
|
|
||||||
return store.state.adminPage.allUserList
|
|
||||||
}),
|
|
||||||
allCountry:[]
|
allCountry:[]
|
||||||
})
|
})
|
||||||
let filterData:any = reactive({
|
let filterData:any = reactive({
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.newFill = new fabric.Rect({
|
this.newFill = new fabric.Rect({
|
||||||
width: clippingMaskFabricObject.width,
|
width: clippingMaskFabricObject.width,
|
||||||
@@ -117,7 +117,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
this.parent.clippingMask,
|
this.parent.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.newFill = new fabric.Rect({
|
this.newFill = new fabric.Rect({
|
||||||
width: clippingMaskFabricObject.width,
|
width: clippingMaskFabricObject.width,
|
||||||
@@ -222,7 +222,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
|
|||||||
this.parent?.clippingMask,
|
this.parent?.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clipPath.clipPath = null;
|
// clipPath.clipPath = null;
|
||||||
clipPath.set({ absolutePositioned: true });
|
clipPath.set({ absolutePositioned: true });
|
||||||
this.group.clipPath = clipPath;
|
this.group.clipPath = clipPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
|
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
// 设置绝对定位
|
// 设置绝对定位
|
||||||
|
|||||||
306
src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
|
|||||||
import { ToolCommand } from "./ToolCommands";
|
import { ToolCommand } from "./ToolCommands";
|
||||||
import {
|
import {
|
||||||
findObjectById,
|
findObjectById,
|
||||||
|
findObjectByLayerId,
|
||||||
generateId,
|
generateId,
|
||||||
getObjectZIndex,
|
getObjectZIndex,
|
||||||
insertObjectAtZIndex,
|
insertObjectAtZIndex,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { restoreFabricObject } from "../utils/objectHelper";
|
import { restoreFabricObject } from "../utils/objectHelper";
|
||||||
|
import EventManager from "../utils/event.js";
|
||||||
/**
|
/**
|
||||||
* 添加图层命令
|
* 添加图层命令
|
||||||
*/
|
*/
|
||||||
@@ -36,7 +37,7 @@ export class AddLayerCommand extends Command {
|
|||||||
|
|
||||||
this.insertIndex = options.insertIndex;
|
this.insertIndex = options.insertIndex;
|
||||||
this.oldActiveLayerId = null;
|
this.oldActiveLayerId = null;
|
||||||
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
|
this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
|
||||||
|
|
||||||
this.options = options.options || {};
|
this.options = options.options || {};
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ export class AddLayerCommand extends Command {
|
|||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
// 从图层列表删除该图层
|
// 从图层列表删除该图层
|
||||||
this.layers.value = [...this.beforeLayers];
|
this.layers.value = JSON.parse(this.beforeLayers);
|
||||||
|
|
||||||
// 恢复原活动图层
|
// 恢复原活动图层
|
||||||
this.activeLayerId.value = this.oldActiveLayerId;
|
this.activeLayerId.value = this.oldActiveLayerId;
|
||||||
@@ -251,12 +252,12 @@ export class PasteLayerCommand extends Command {
|
|||||||
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
|
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
// 添加所有对象到画布
|
// 添加所有对象到画布
|
||||||
allObjects.forEach((obj) => {
|
allObjects.forEach((obj) => {
|
||||||
@@ -523,6 +524,7 @@ export class RemoveLayerCommand extends Command {
|
|||||||
this.layerId = options.layerId;
|
this.layerId = options.layerId;
|
||||||
this.activeLayerId = options.activeLayerId;
|
this.activeLayerId = options.activeLayerId;
|
||||||
this.layerManager = options.layerManager || null;
|
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(
|
this.layerIndex = this.layers.value.findIndex(
|
||||||
@@ -599,7 +601,9 @@ export class RemoveLayerCommand extends Command {
|
|||||||
);
|
);
|
||||||
// 从图层列表中删除
|
// 从图层列表中删除
|
||||||
this.layers.value.splice(this.layerIndex, 1);
|
this.layers.value.splice(this.layerIndex, 1);
|
||||||
|
if(this.IsOnlyLayer){
|
||||||
|
this.addCmd = await this.layerManager?.createLayer?.(null, LayerType.EMPTY, {}, false);
|
||||||
|
}
|
||||||
// 如果删除的是当前活动图层,需要更新活动图层
|
// 如果删除的是当前活动图层,需要更新活动图层
|
||||||
if (this.isActiveLayer) {
|
if (this.isActiveLayer) {
|
||||||
// 查找最近的非背景层作为新的活动图层
|
// 查找最近的非背景层作为新的活动图层
|
||||||
@@ -632,6 +636,9 @@ export class RemoveLayerCommand extends Command {
|
|||||||
async undo() {
|
async undo() {
|
||||||
// 恢复图层到原位置
|
// 恢复图层到原位置
|
||||||
if (this.layerIndex !== -1 && this.removedLayer) {
|
if (this.layerIndex !== -1 && this.removedLayer) {
|
||||||
|
if(this.IsOnlyLayer && this.addCmd){
|
||||||
|
this.addCmd?.undo?.();
|
||||||
|
}
|
||||||
this.layers.value.splice(this.layerIndex, 0, this.removedLayer);
|
this.layers.value.splice(this.layerIndex, 0, this.removedLayer);
|
||||||
|
|
||||||
// 使用优化渲染批处理恢复真实对象到画布
|
// 使用优化渲染批处理恢复真实对象到画布
|
||||||
@@ -649,7 +656,6 @@ export class RemoveLayerCommand extends Command {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
|
||||||
@@ -802,15 +808,23 @@ export class ToggleLayerVisibilityCommand extends Command {
|
|||||||
|
|
||||||
// 切换可见性
|
// 切换可见性
|
||||||
this.layer.visible = !this.layer.visible;
|
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) {
|
if (this.canvas) {
|
||||||
const layerObjects = this.canvas
|
this.canvas.getObjects().forEach((obj) => {
|
||||||
.getObjects()
|
if (ids.includes(obj.layerId)) {
|
||||||
.filter((obj) => obj.layerId === this.layerId);
|
obj.getObjects?.()?.forEach((item) => {
|
||||||
layerObjects.forEach((obj) => {
|
item.visible = this.layer.visible;
|
||||||
obj.visible = this.layer.visible;
|
});
|
||||||
});
|
obj.visible = this.layer.visible;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 更新画布上对象的可选择状态
|
// 更新画布上对象的可选择状态
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity();
|
await this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
@@ -868,13 +882,14 @@ export class ToggleChildLayerVisibilityCommand extends Command {
|
|||||||
|
|
||||||
// 更新画布上图层对象的可见性
|
// 更新画布上图层对象的可见性
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
const layerObjects = this.canvas
|
this.canvas.getObjects().forEach((obj) => {
|
||||||
.getObjects()
|
if (obj.layerId === this.layerId) {
|
||||||
.filter((obj) => obj.layerId === this.layerId);
|
obj.getObjects?.()?.forEach((item) => {
|
||||||
|
item.visible = this.childLayer.visible;
|
||||||
layerObjects.forEach((obj) => {
|
});
|
||||||
obj.visible = this.childLayer.visible;
|
obj.visible = this.childLayer.visible;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新画布上对象的可选择状态
|
// 更新画布上对象的可选择状态
|
||||||
@@ -1007,9 +1022,8 @@ export class LayerLockCommand extends Command {
|
|||||||
|
|
||||||
// 如果是组图层,递归更新所有子图层
|
// 如果是组图层,递归更新所有子图层
|
||||||
if (
|
if (
|
||||||
layer.type === "group" &&
|
|
||||||
layer.children &&
|
layer.children &&
|
||||||
Array.isArray(layer.children)
|
Array.isArray(layer.children) && layer.children.length > 0
|
||||||
) {
|
) {
|
||||||
layer.children.forEach((child) => {
|
layer.children.forEach((child) => {
|
||||||
this._updateLayerLockState(child, locked);
|
this._updateLayerLockState(child, locked);
|
||||||
@@ -1108,7 +1122,7 @@ export class SetLayerOpacityCommand extends Command {
|
|||||||
|
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
|
EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,6 +1144,7 @@ export class SetLayerOpacityCommand extends Command {
|
|||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo() {
|
getInfo() {
|
||||||
@@ -1371,7 +1386,7 @@ export class GroupLayersCommand extends Command {
|
|||||||
// 备份原图层
|
// 备份原图层
|
||||||
this.originalLayers = [...this.layers.value];
|
this.originalLayers = [...this.layers.value];
|
||||||
// 新组ID
|
// 新组ID
|
||||||
this.groupId =
|
this.groupId = options.id ||
|
||||||
generateId("group_layer_") ||
|
generateId("group_layer_") ||
|
||||||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||||
|
|
||||||
@@ -4276,24 +4291,28 @@ export class RemoveChildLayerCommand extends Command {
|
|||||||
}
|
}
|
||||||
// 恢复子图层到原位置
|
// 恢复子图层到原位置
|
||||||
this.parentLayer.children.splice(this.childIndex, 0, this.removedChild);
|
this.parentLayer.children.splice(this.childIndex, 0, this.removedChild);
|
||||||
optimizeCanvasRendering(this.canvas, async () => {
|
await new Promise((resolve) => {
|
||||||
this.originalObjects.forEach((obj) => {
|
optimizeCanvasRendering(this.canvas, async () => {
|
||||||
// 恢复对象到画布
|
this.originalObjects.forEach((obj) => {
|
||||||
this.canvas.add(obj);
|
// 恢复对象到画布
|
||||||
// 恢复对象的图层信息
|
this.canvas.add(obj);
|
||||||
obj.layerId = this.layerId;
|
// 恢复对象的图层信息
|
||||||
obj.layerName = this.removedChild.name;
|
obj.layerId = this.layerId;
|
||||||
obj.setCoords(); // 更新坐标
|
obj.layerName = this.removedChild.name;
|
||||||
});
|
obj.setCoords(); // 更新坐标
|
||||||
|
});
|
||||||
|
|
||||||
// 如果是原活动图层,恢复活动图层
|
// 如果是原活动图层,恢复活动图层
|
||||||
if (this.isActiveLayer) {
|
if (this.isActiveLayer) {
|
||||||
this.activeLayerId.value = this.layerId;
|
this.activeLayerId.value = this.layerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新渲染画布
|
// 重新渲染画布
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
await this.layerManager?.updateLayersObjectsInteractivity(false);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo() {
|
getInfo() {
|
||||||
@@ -4434,3 +4453,90 @@ export class ChildLayerLockCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 设置图层混合模式
|
||||||
|
*/
|
||||||
|
export class SetLayerCompositeCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: "设置图层混合模式",
|
||||||
|
saveState: false,
|
||||||
|
});
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layers = options.layers;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.layerId = options.layerId;
|
||||||
|
this.newValue = options.newValue;
|
||||||
|
this.oldValue = options.oldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(isUndo = false) {
|
||||||
|
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
|
||||||
|
const { object } = findObjectByLayerId(this.canvas, this.layerId);
|
||||||
|
if (!layer || !object) {
|
||||||
|
console.error(`图层${this.layerId}不存在`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// console.log("==========", this.newValue, this.oldValue);
|
||||||
|
const value = isUndo ? this.oldValue : this.newValue;
|
||||||
|
layer.blendMode = value;
|
||||||
|
object.set("globalCompositeOperation", value);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
const event = isUndo ? "object:composite:undo" : "object:composite:execute";
|
||||||
|
EventManager.emit(event, object);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
return this.execute(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置颜色图层颜色
|
||||||
|
*/
|
||||||
|
export class SetColorLayerFillCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: "设置颜色图层颜色",
|
||||||
|
saveState: false,
|
||||||
|
});
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.layerManager = options.layerManager;
|
||||||
|
this.object = options.object;
|
||||||
|
this.layer = this.layerManager?.getLayerById(this.object.layerId);
|
||||||
|
this.newFill = options.newFill;
|
||||||
|
this.oldFill = JSON.parse(JSON.stringify(this.object.fill));
|
||||||
|
this.layer.blendMode = "multiply";
|
||||||
|
this.object.set("globalCompositeOperation", "multiply");
|
||||||
|
this.object.set("originColor", options.originColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(isUndo = false) {
|
||||||
|
if (!this.object) {
|
||||||
|
console.error(`颜色图层不存在`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isVisible = this.layer?.visible;
|
||||||
|
if(!isVisible && this.layer) this.layer.visible = true;
|
||||||
|
const gradient = new fabric.Gradient({
|
||||||
|
type: "linear",
|
||||||
|
gradientUnits: "percentage",
|
||||||
|
...(isUndo ? this.oldFill : this.newFill),
|
||||||
|
});
|
||||||
|
this.object.setFill(gradient);
|
||||||
|
this.canvas.renderAll();
|
||||||
|
await this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
|
||||||
|
this.object.id
|
||||||
|
);
|
||||||
|
if(!isVisible && this.layer) this.layer.visible = false;
|
||||||
|
this.layerManager?.updateLayersObjectsInteractivity();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
this.execute(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/component/Canvas/CanvasEditor/commands/ObjectCommands.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Command } from "./Command.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象移动命令
|
||||||
|
* 轻量级命令,只记录对象的移动属性变化(位置)
|
||||||
|
*/
|
||||||
|
export class ObjectMoveCommand extends Command {
|
||||||
|
constructor(options) {
|
||||||
|
super({
|
||||||
|
name: options.name || "对象移动",
|
||||||
|
description: options.description || "移动对象",
|
||||||
|
saveState: false, // 自己管理状态,避免递归
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas = options.canvas;
|
||||||
|
this.initPos = options.initPos;
|
||||||
|
this.finalPos = options.finalPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令
|
||||||
|
*/
|
||||||
|
async execute() {
|
||||||
|
this.setObjectsPos(this.finalPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 撤销命令
|
||||||
|
* 应用初始状态
|
||||||
|
*/
|
||||||
|
async undo() {
|
||||||
|
this.setObjectsPos(this.initPos);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setObjectsPos(pos) {
|
||||||
|
const objects = this.canvas.getObjects();
|
||||||
|
const arr = typeof pos === "object" ? [pos] : pos;
|
||||||
|
arr.forEach((item) => {
|
||||||
|
const obj = objects.find((o) => o.id === item.id);
|
||||||
|
if(obj) {
|
||||||
|
obj.set({
|
||||||
|
left: item.left,
|
||||||
|
top: item.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.canvas.renderAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,7 +234,7 @@ export class AddObjectToLayerCommand extends Command {
|
|||||||
parent.clippingMask,
|
parent.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({ absolutePositioned: true });
|
clippingMaskFabricObject.set({ absolutePositioned: true });
|
||||||
this.fabricObject.clipPath = clippingMaskFabricObject;
|
this.fabricObject.clipPath = clippingMaskFabricObject;
|
||||||
// 标记为脏对象
|
// 标记为脏对象
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
this.layerId
|
this.layerId
|
||||||
);
|
);
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
this.parentLayer = parent;
|
// this.parentLayer = parent;
|
||||||
|
|
||||||
// 新增:如果有父图层,则栅格化父图层及其所有子图层
|
// // 新增:如果有父图层,则栅格化父图层及其所有子图层
|
||||||
if (this.parentLayer) {
|
// if (this.parentLayer) {
|
||||||
this.layer = this.parentLayer;
|
// this.layer = this.parentLayer;
|
||||||
this.layerId = this.parentLayer.id;
|
// this.layerId = this.parentLayer.id;
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 恢复原始图层结构
|
// 恢复原始图层结构
|
||||||
this.layers.value = [...this.originalLayerStructure];
|
this.layers.value = JSON.parse(this.originalLayerStructure);
|
||||||
|
|
||||||
// 恢复原活动图层
|
// 恢复原活动图层
|
||||||
this.activeLayerId.value = this.layerId;
|
this.activeLayerId.value = this.layerId;
|
||||||
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
_saveOriginalLayerStructure() {
|
_saveOriginalLayerStructure() {
|
||||||
// 只保存相关的图层结构,而不是整个图层数组
|
// 只保存相关的图层结构,而不是整个图层数组
|
||||||
this.originalLayerStructure = JSON.parse(JSON.stringify(this.layers.value));
|
this.originalLayerStructure = JSON.stringify(this.layers.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -517,12 +517,12 @@ export class ExportLayerToImageCommand extends Command {
|
|||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { findObjectById } from "../utils/helper";
|
|||||||
import { findLayerRecursively } from "../utils/layerHelper";
|
import { findLayerRecursively } from "../utils/layerHelper";
|
||||||
import { restoreFabricObject } from "../utils/objectHelper";
|
import { restoreFabricObject } from "../utils/objectHelper";
|
||||||
import { Command } from "./Command";
|
import { Command } from "./Command";
|
||||||
|
import EventManager from "../utils/event.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对象变换命令
|
* 对象变换命令
|
||||||
@@ -75,7 +76,7 @@ export class TransformCommand extends Command {
|
|||||||
|
|
||||||
// 触发画布更新
|
// 触发画布更新
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
EventManager.emit("object:modified:execute", targetObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ export class TransformCommand extends Command {
|
|||||||
}, 300);
|
}, 300);
|
||||||
// 触发画布更新
|
// 触发画布更新
|
||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
|
EventManager.emit("object:modified:undo", targetObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ export class TransformCommand extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (clippingMaskFabricObject) {
|
if (clippingMaskFabricObject) {
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 图片列表面板 -->
|
|
||||||
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
|
<div v-if="showPanel" class="crop-image-overlay" @click.self="close">
|
||||||
<div class="crop-image-modal">
|
<div class="crop-image-modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -392,7 +391,7 @@
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
/* 弹窗遮罩层 */
|
/* 弹窗遮罩层 */
|
||||||
.crop-image-overlay {
|
.crop-image-overlay {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -420,8 +419,8 @@
|
|||||||
.crop-image-modal {
|
.crop-image-modal {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 80%;
|
width: 90%;
|
||||||
height: 80%;
|
height: 90%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
import { ref, nextTick, computed, inject } from "vue";
|
import { ref, nextTick, computed, inject } from "vue";
|
||||||
import { Checkbox } from "ant-design-vue";
|
import { Checkbox } from "ant-design-vue";
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { isGroupLayer } from "../../utils/layerHelper";
|
import { isGroupLayer, SpecialLayerId } from "../../utils/layerHelper";
|
||||||
|
import { fillToCssStyle, palletToFill, fillToPallet } from "../../utils/helper";
|
||||||
|
import { SetColorLayerFillCommand } from "../../commands/LayerCommands";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
// 设置组件名称,用于递归渲染
|
// 设置组件名称,用于递归渲染
|
||||||
@@ -183,6 +185,9 @@ function handleToggleVisibility() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleLock() {
|
function handleToggleLock() {
|
||||||
|
// 禁用解锁的图层不能操作
|
||||||
|
if (props.layer.isDisableUnlock) return;
|
||||||
|
|
||||||
if (props.isChild) {
|
if (props.isChild) {
|
||||||
// 子图层需要传递父图层ID - 从父级组件获取
|
// 子图层需要传递父图层ID - 从父级组件获取
|
||||||
const parentId = props.layer.parentId || findParentLayerId();
|
const parentId = props.layer.parentId || findParentLayerId();
|
||||||
@@ -348,6 +353,29 @@ function findParentLayerId() {
|
|||||||
console.warn("无法找到图层的父图层:", props.layer.id);
|
console.warn("无法找到图层的父图层:", props.layer.id);
|
||||||
return null;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -377,8 +405,8 @@ function findParentLayerId() {
|
|||||||
@contextmenu.prevent="handleContextMenu"
|
@contextmenu.prevent="handleContextMenu"
|
||||||
>
|
>
|
||||||
<!-- 拖拽手柄 -->
|
<!-- 拖拽手柄 -->
|
||||||
<div class="layer-drag-handle" :title="$t('拖拽排序')">
|
<div class="layer-drag-handle" :title="$t('拖拽排序')" v-if="!isHidenDragHandle">
|
||||||
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
<SvgIcon :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图层头部 -->
|
<!-- 图层头部 -->
|
||||||
@@ -417,9 +445,18 @@ function findParentLayerId() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 颜色图层按钮 -->
|
||||||
|
<div
|
||||||
|
class="layer-color-btn"
|
||||||
|
v-if="layer.id === SpecialLayerId.COLOR"
|
||||||
|
@click.stop="clickColor"
|
||||||
|
:style="{
|
||||||
|
background: fillToCssStyle(layerObject.fill),
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- 图层操作按钮 -->
|
<!-- 图层操作按钮 -->
|
||||||
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
|
<div class="layer-actions" >
|
||||||
<!-- 可见性切换 -->
|
<!-- 可见性切换 -->
|
||||||
<div
|
<div
|
||||||
class="visibility-btn"
|
class="visibility-btn"
|
||||||
@@ -434,7 +471,7 @@ function findParentLayerId() {
|
|||||||
<span
|
<span
|
||||||
v-if="layer.locked"
|
v-if="layer.locked"
|
||||||
class="status-icon locked"
|
class="status-icon locked"
|
||||||
:class="{ disabled: layer.isBackground || layer.isFixed }"
|
:class="{ disabled: layer.isBackground || layer.isFixed || layer.isDisableUnlock || layer.isFixedOther }"
|
||||||
:title="$t('锁定')"
|
:title="$t('锁定')"
|
||||||
@click.stop="handleToggleLock"
|
@click.stop="handleToggleLock"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ const fillColorRef = ref(null);
|
|||||||
// 计算属性:可排序的根级图层(排除背景层和固定层)
|
// 计算属性:可排序的根级图层(排除背景层和固定层)
|
||||||
const sortableRootLayers = computed(() => {
|
const sortableRootLayers = computed(() => {
|
||||||
if (!layers) return [];
|
if (!layers) return [];
|
||||||
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
|
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground && !layer.isFixedOther);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:不可排序的固定图层(背景层和固定层)
|
// 计算属性:不可排序的固定图层(背景层和固定层)
|
||||||
const fixedLayers = computed(() => {
|
const fixedLayers = computed(() => {
|
||||||
if (!layers) return [];
|
if (!layers) return [];
|
||||||
return layers.value.filter((layer) => {
|
return layers.value.filter((layer) => {
|
||||||
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
|
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground || layer.isFixedOther);
|
||||||
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
|
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -576,7 +576,7 @@ function handleLayerClick(layer, event) {
|
|||||||
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
|
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
|
||||||
toggleLayerSelection(layer, event);
|
toggleLayerSelection(layer, event);
|
||||||
} else {
|
} else {
|
||||||
lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
if(!layer.isFixedClipMask) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
|
||||||
// 普通点击:进入单选模式
|
// 普通点击:进入单选模式
|
||||||
// selectedLayerIds.value = [layer.id];
|
// selectedLayerIds.value = [layer.id];
|
||||||
// isMultiSelectMode.value = false;
|
// isMultiSelectMode.value = false;
|
||||||
@@ -596,7 +596,7 @@ function handleLayerClick(layer, event) {
|
|||||||
layerManager?.updateLayersObjectsInteractivity();
|
layerManager?.updateLayersObjectsInteractivity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
if(!layer.isFixedClipMask) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,7 +999,7 @@ function buildChildLayerContextMenuItems(childLayer) {
|
|||||||
{
|
{
|
||||||
label: childLayer.locked ? "解锁图层" : "锁定图层",
|
label: childLayer.locked ? "解锁图层" : "锁定图层",
|
||||||
icon: childLayer.locked ? "CUnLock" : "CLock",
|
icon: childLayer.locked ? "CUnLock" : "CLock",
|
||||||
disabled: childLayer.isBackground || childLayer.isFixed,
|
disabled: childLayer.isBackground || childLayer.isFixed || childLayer.isDisableUnlock,
|
||||||
action: () => toggleChildLayerLock(childLayer.id),
|
action: () => toggleChildLayerLock(childLayer.id),
|
||||||
},
|
},
|
||||||
// 显示/隐藏
|
// 显示/隐藏
|
||||||
@@ -1633,7 +1633,6 @@ async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex
|
|||||||
@delete-child="deleteChildLayer"
|
@delete-child="deleteChildLayer"
|
||||||
@rename-child="renameChildLayer"
|
@rename-child="renameChildLayer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 固定层(背景层和固定层) -->
|
<!-- 固定层(背景层和固定层) -->
|
||||||
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
<div v-if="fixedLayers.length > 0" class="fixed-layers">
|
||||||
<!-- 遍历固定层 -->
|
<!-- 遍历固定层 -->
|
||||||
|
|||||||
@@ -340,6 +340,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layer-color-btn{
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
// 图层操作
|
// 图层操作
|
||||||
.layer-actions {
|
.layer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ async function prepareForLiquify(targetObj) {
|
|||||||
}
|
}
|
||||||
updateAllParams();
|
updateAllParams();
|
||||||
|
|
||||||
console.log("液化环境准备完成");
|
console.log("液化环境准备完成",compositeCommand);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("准备液化环境失败:", error);
|
console.error("准备液化环境失败:", error);
|
||||||
@@ -1614,6 +1614,7 @@ function close() {
|
|||||||
*/
|
*/
|
||||||
function startPressTimer() {
|
function startPressTimer() {
|
||||||
if (pressTimer.value) return;
|
if (pressTimer.value) return;
|
||||||
|
if (currentMode.value === compositeCommand.value.liquifyManager.enhancedManager.modes.PUSH) return;
|
||||||
|
|
||||||
pressTimer.value = setInterval(() => {
|
pressTimer.value = setInterval(() => {
|
||||||
// 计算按压持续时间
|
// 计算按压持续时间
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 颜色选择器模板 -->
|
||||||
|
<div v-show="showPanel" class="pallet-overlay" @click.self="close">
|
||||||
|
<div class="pallet-modal">
|
||||||
|
<!-- <div class="modal-header">
|
||||||
|
<h3></h3>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<pallet
|
||||||
|
v-if="showPanel"
|
||||||
|
:selectColor="selectColor"
|
||||||
|
@selectUplpadColor="selectUplpadColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="image-count" @click="close">
|
||||||
|
{{ $t("Canvas.close") }}
|
||||||
|
</div>
|
||||||
|
<div class="image-submit gallery_btn" @click="confirm">
|
||||||
|
{{ $t("Canvas.confirm") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
defineProps,
|
||||||
|
onDeactivated,
|
||||||
|
reactive,
|
||||||
|
onMounted,
|
||||||
|
defineExpose,
|
||||||
|
nextTick,
|
||||||
|
onUnmounted,
|
||||||
|
} from "vue";
|
||||||
|
import pallet from "./pallet.vue";
|
||||||
|
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps([]);
|
||||||
|
const { t } = useI18n();
|
||||||
|
var resolveFn: (value: any) => void;
|
||||||
|
const showPanel = ref(false);
|
||||||
|
const open = (obj = {}) => {
|
||||||
|
selectColor.value = JSON.parse(JSON.stringify(obj));
|
||||||
|
showPanel.value = true;
|
||||||
|
return new Promise((resolve) => (resolveFn = resolve));
|
||||||
|
};
|
||||||
|
const close = () => {
|
||||||
|
showPanel.value = false;
|
||||||
|
};
|
||||||
|
//提交选中的T图片
|
||||||
|
const confirm = () => {
|
||||||
|
close();
|
||||||
|
resolveFn && resolveFn(JSON.parse(JSON.stringify(selectColor.value)));
|
||||||
|
};
|
||||||
|
const selectColor = ref({});
|
||||||
|
const selectUplpadColor = (item: any) => {
|
||||||
|
selectColor.value = JSON.parse(JSON.stringify(item));
|
||||||
|
};
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
/* 弹窗遮罩层 */
|
||||||
|
.pallet-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1001;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗主体 */
|
||||||
|
.pallet-modal {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 95%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||||
|
animation: modalSlideUp 0.3s ease;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗头部 */
|
||||||
|
.modal-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #333;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗内容 */
|
||||||
|
.modal-content {
|
||||||
|
width: 35rem;
|
||||||
|
// max-width: 240px;
|
||||||
|
margin: 10px 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗底部 */
|
||||||
|
.modal-footer {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
// border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
> .image-submit {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-count {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,666 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pallet" ref="palletRef">
|
||||||
|
<div class="palletColo" @click="openPallet">
|
||||||
|
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
||||||
|
{{ selectColor.hex }}
|
||||||
|
</div>
|
||||||
|
<div v-show="selectColor.gradient" class="palletBackColor" :style="{'background-image':`linear-gradient(${selectColor.gradient?.angle}deg,${setGradient(selectColor.gradient)})`}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="palletBox">
|
||||||
|
<div class="color_setting_block" @click.stop>
|
||||||
|
<Chrome class="chrome_color" v-model="color_"></Chrome>
|
||||||
|
<div class="color_setting_operateSingle">
|
||||||
|
<div class="color_setting_btn" :class="{active:!color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Single') }}</div>
|
||||||
|
<a-switch :checked="color?.gradient?.gradientShow" @click="setOperate"/>
|
||||||
|
<div class="color_setting_btn" :class="{active:color?.gradient?.gradientShow}">{{ $t('ColorboardUpload.Gradual') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="color_setting_operate" v-if="color?.gradient?.gradientShow">
|
||||||
|
<div class="color_setting_operate_item color_setting_operate_control">
|
||||||
|
<div class="operate_item_box">
|
||||||
|
<div>{{ $t('ColorboardUpload.Alignment') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="operate_item_box operate_item_angle">
|
||||||
|
<div class="operate_item_angle_box" @mousedown="mousedownGradientAngle(getMousePosition($event,false))" @touchstart="mousedownGradientAngle(getMousePosition($event,true))">
|
||||||
|
<div :style="{'transform':`rotate(${color.gradient.angle}deg)`}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="operate_item_box operate_item_delete">
|
||||||
|
<i class="fi fi-rr-trash" @click="deleteGradientItem"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="color_setting_operate_item color_setting_operate_input">
|
||||||
|
<div class="color_setting_operate_bg" @click="addGradient($event)" :style="{'background-image':color?.gradient?`linear-gradient(90deg,${setGradient(color.gradient)})`:'none'}">
|
||||||
|
</div>
|
||||||
|
<div v-for="item,index in color.gradient.gradientList" :key="item" class="color_setting_operate_btn" :class="{'active':index == color.gradient.selectIndex}" :style="{'left':item.left,'background-color':`rgba(${item.rgba.r},${item.rgba.g},${item.rgba.b},${item.rgba.a})`}" @mousedown="mousedownGradient(getMousePosition($event,false),item,index,)" @touchstart="mousedownGradient(getMousePosition($event,true),item,index,)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent,computed,ref,watch,nextTick,onMounted,onUnmounted,toRefs, reactive} from 'vue'
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { message,Upload} from 'ant-design-vue';
|
||||||
|
import { Sketch, Chrome} from '@ans1998/vue3-color'
|
||||||
|
import { getMousePosition } from "@/tool/mdEvent";
|
||||||
|
import { rgbaToHex } from "@/tool/util"
|
||||||
|
import { color } from 'echarts/core';
|
||||||
|
export default defineComponent({
|
||||||
|
components:{
|
||||||
|
Chrome,
|
||||||
|
},
|
||||||
|
props:{
|
||||||
|
selectColor:{
|
||||||
|
type:Object,
|
||||||
|
default:()=>{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits:['selectUplpadColor'],
|
||||||
|
setup(props,{emit}) {
|
||||||
|
const {t} = useI18n()
|
||||||
|
const store = useStore();
|
||||||
|
const palletData = reactive({
|
||||||
|
palletShow: true,
|
||||||
|
palletList:[],
|
||||||
|
color_:{} as any,
|
||||||
|
color:{} as any,
|
||||||
|
updataSelectColorTime:null as any,
|
||||||
|
gradient:{
|
||||||
|
gradientList:[
|
||||||
|
{
|
||||||
|
rgba:{
|
||||||
|
r:117,
|
||||||
|
g:119,
|
||||||
|
b:255,
|
||||||
|
a:1,
|
||||||
|
},
|
||||||
|
left:'0%'
|
||||||
|
},{
|
||||||
|
rgba:{
|
||||||
|
r:0,
|
||||||
|
g:222,
|
||||||
|
b:152,
|
||||||
|
a:1,
|
||||||
|
},
|
||||||
|
left:'100%'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
angle:45,
|
||||||
|
selectIndex:-1,
|
||||||
|
gradientShow:false,
|
||||||
|
},
|
||||||
|
setGradient:computed(()=>{
|
||||||
|
return (gradient:any)=>{
|
||||||
|
let gradientStr = ''
|
||||||
|
if(!gradient?.gradientList)return
|
||||||
|
gradient.gradientList.sort((a:any, b:any) => {
|
||||||
|
let aArr = a.left.split('%')[0]
|
||||||
|
let bArr = b.left.split('%')[0]
|
||||||
|
return aArr - bArr;
|
||||||
|
});
|
||||||
|
gradient.gradientList.forEach((item:any,index:any)=>{
|
||||||
|
let str = ','
|
||||||
|
if(gradient.gradientList.length == index+1)str = ''
|
||||||
|
let rgba = item.rgba?item.rgba:{r:255,g:255,b:255}
|
||||||
|
gradientStr += `rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a}) ${item.left}${str}`
|
||||||
|
|
||||||
|
})
|
||||||
|
return `${gradientStr}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
const getpalletListDom = reactive({
|
||||||
|
})
|
||||||
|
const palletRef = ref(null)
|
||||||
|
watch(()=>palletData.color_,(newVal:any)=>{
|
||||||
|
if(!newVal?.rgba?.r)return
|
||||||
|
if(palletData.color?.gradient?.gradientShow){
|
||||||
|
palletData.color.gradient.gradientList[palletData.color.gradient.selectIndex].rgba = {
|
||||||
|
r:newVal.rgba.r,
|
||||||
|
g:newVal.rgba.g,
|
||||||
|
b:newVal.rgba.b,
|
||||||
|
a:newVal.rgba.a,
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
palletData.color = newVal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(()=>palletData.color,(newVal:any)=>{
|
||||||
|
if(JSON.stringify(props.selectColor) != JSON.stringify(newVal)){
|
||||||
|
newVal.name = ''
|
||||||
|
newVal.tcx = ''
|
||||||
|
let rgba = [newVal.rgba.r,newVal.rgba.g,newVal.rgba.b]
|
||||||
|
let hex = rgbaToHex(rgba)
|
||||||
|
newVal.hex = hex
|
||||||
|
emit('selectUplpadColor',newVal)
|
||||||
|
}
|
||||||
|
},{deep: true })
|
||||||
|
const setOperate = ()=>{
|
||||||
|
if(!palletData.color.rgba)return message.info(t('DesignDetailAlter.jsContent7'))
|
||||||
|
palletData.color.rgba = palletData.color?.rgba?.r?palletData.color.rgba:{r:0,g:0,b:0,a:1}
|
||||||
|
palletData.gradient.selectIndex = 0
|
||||||
|
palletData.gradient.gradientShow = true
|
||||||
|
if(!palletData.color.gradient){
|
||||||
|
if(palletData.color.rgba.r){
|
||||||
|
palletData.gradient.gradientList[palletData.gradient.selectIndex].rgba = {
|
||||||
|
r:palletData.color.rgba.r,
|
||||||
|
g:palletData.color.rgba.g,
|
||||||
|
b:palletData.color.rgba.b,
|
||||||
|
a:1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
palletData.color.gradient = JSON.parse(JSON.stringify(palletData.gradient))
|
||||||
|
}else{
|
||||||
|
palletData.color.rgba = palletData.color.gradient.gradientList[0].rgba
|
||||||
|
palletData.color.gradient = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deleteGradientItem = ()=>{
|
||||||
|
if(palletData.color.gradient.gradientList.length <= 2)return
|
||||||
|
palletData.color.gradient.gradientList.splice(palletData.color.gradient.selectIndex,1)
|
||||||
|
}
|
||||||
|
const addGradient = (event:any)=>{
|
||||||
|
let gradientWidth = event.target.clientWidth
|
||||||
|
let left:any = event.offsetX/gradientWidth
|
||||||
|
palletData.color.gradient.gradientList.push({
|
||||||
|
rgba:palletData.color_.rgba,
|
||||||
|
left:left.toFixed(2)*100+'%'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const mousedownGradientAngle = (event:any)=>{
|
||||||
|
// isMoible() true为移动端
|
||||||
|
let domPosition = event.target.getBoundingClientRect()
|
||||||
|
let position = {
|
||||||
|
x:domPosition.x+domPosition.width/2,
|
||||||
|
y:domPosition.y+domPosition.height/2,
|
||||||
|
}
|
||||||
|
let angle
|
||||||
|
let mousedown = function(event:any){
|
||||||
|
let e = getMousePosition(event,false)
|
||||||
|
mouseDownOperation(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchstart = function(event:any){
|
||||||
|
let e = getMousePosition(event,true)
|
||||||
|
mouseDownOperation(e)
|
||||||
|
}
|
||||||
|
let mouseDownOperation = (e:any)=>{
|
||||||
|
let X = position.x
|
||||||
|
let Y = position.y
|
||||||
|
let x = (e.clientX) - X
|
||||||
|
let y = Y -( e.clientY)
|
||||||
|
angle = Math.atan2(x,y)*(180 / Math.PI)
|
||||||
|
// this.colorList[this.selectIndex].gradient = JSON.parse(JSON.stringify(this.gradient))
|
||||||
|
palletData.color.gradient.angle = angle
|
||||||
|
|
||||||
|
}
|
||||||
|
let mouseupGradientAngle = ()=>{
|
||||||
|
window.removeEventListener('touchmove',touchstart)
|
||||||
|
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove',mousedown)
|
||||||
|
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
window.addEventListener('touchmove',touchstart)
|
||||||
|
window.addEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.addEventListener('mousemove',mousedown)
|
||||||
|
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousedownGradient = (event:any,item:any,index:number)=>{
|
||||||
|
palletData.color.gradient.selectIndex = index
|
||||||
|
|
||||||
|
|
||||||
|
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
||||||
|
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
|
||||||
|
let position = {
|
||||||
|
x:event.clientX,
|
||||||
|
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
||||||
|
}
|
||||||
|
let mousedown = function(event:any){
|
||||||
|
let e = getMousePosition(event,false)
|
||||||
|
mousedownGradient(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchstart = function(event:any){
|
||||||
|
let e = getMousePosition(event,true)
|
||||||
|
mousedownGradient(e)
|
||||||
|
}
|
||||||
|
let mousedownGradient = (e:any)=>{
|
||||||
|
let left = ((e.clientX) - position.x)/gradientWidth*100+Number(position.left)
|
||||||
|
left = (left<0?0:left>100?100:left)
|
||||||
|
item.left = left+'%'
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseupGradientAngle = ()=>{
|
||||||
|
window.removeEventListener('touchmove',touchstart)
|
||||||
|
window.removeEventListener('touchend',mouseupGradientAngle)
|
||||||
|
window.removeEventListener('mousemove',mousedown)
|
||||||
|
window.removeEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
}
|
||||||
|
window.addEventListener('touchmove',touchstart)
|
||||||
|
window.addEventListener('touchend',mouseupGradientAngle)
|
||||||
|
|
||||||
|
window.addEventListener('mousemove',mousedown)
|
||||||
|
window.addEventListener('mouseup',mouseupGradientAngle)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
const selectImgItem = ()=>{
|
||||||
|
|
||||||
|
}
|
||||||
|
const openPallet = ()=>{
|
||||||
|
if(palletData.palletShow && props.selectColor?.rgba?.r){
|
||||||
|
if(props.selectColor.gradient){
|
||||||
|
palletData.color_.rgba = props.selectColor.gradient.gradientList[0].rgba
|
||||||
|
}else{
|
||||||
|
palletData.color_ = JSON.parse(JSON.stringify(props.selectColor))
|
||||||
|
palletData.gradient.gradientShow = false
|
||||||
|
}
|
||||||
|
|
||||||
|
palletData.color = JSON.parse(JSON.stringify(props.selectColor))
|
||||||
|
}else{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 点击外部区域关闭颜色选择器
|
||||||
|
const handleClickOutside = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
|
||||||
|
const palletColo = palletRef.value.querySelector('.palletColo');
|
||||||
|
|
||||||
|
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
||||||
|
if (palletData.palletShow && colorSettingBlock &&
|
||||||
|
!colorSettingBlock.contains(target) &&
|
||||||
|
!palletColo?.contains(target)) {
|
||||||
|
palletData.palletShow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(()=>{
|
||||||
|
// 添加点击外部区域监听器
|
||||||
|
// document.addEventListener('click', handleClickOutside);
|
||||||
|
|
||||||
|
nextTick().then(()=>{
|
||||||
|
const backIcon = document.createElement('div');
|
||||||
|
backIcon.classList.add('vc-sketch-color-wrap')
|
||||||
|
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
||||||
|
dropperDom.appendChild(backIcon);
|
||||||
|
backIcon.addEventListener('click',async ()=>{
|
||||||
|
try {
|
||||||
|
const dropper = new EyeDropper();
|
||||||
|
const result = await dropper.open();
|
||||||
|
let hex = result.sRGBHex.replace("#", "");
|
||||||
|
// 将十六进制颜色码拆分成红、绿、蓝三个部分
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
palletData.color = {rgba:{r:r,g:g,b:b,a:1},hex:result.sRGBHex}
|
||||||
|
// return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
// box.style.backgroundColor = label.textContent = result.sRGBHex;
|
||||||
|
} catch (e) {
|
||||||
|
message.info(t('DesignDetailAlter.jsContent1'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
openPallet();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(()=>{
|
||||||
|
// 清理事件监听器
|
||||||
|
// document.removeEventListener('click', handleClickOutside);
|
||||||
|
})
|
||||||
|
return{
|
||||||
|
...toRefs(palletData),
|
||||||
|
...toRefs(getpalletListDom),
|
||||||
|
palletRef,
|
||||||
|
openPallet,
|
||||||
|
selectImgItem,
|
||||||
|
setOperate,
|
||||||
|
deleteGradientItem,
|
||||||
|
addGradient,
|
||||||
|
mousedownGradientAngle,
|
||||||
|
mousedownGradient,
|
||||||
|
getMousePosition,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.pallet{
|
||||||
|
// position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
user-select: none;
|
||||||
|
> .palletColo{
|
||||||
|
width: 100%;
|
||||||
|
height: 7rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: .5rem .6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
> .palletBackColor{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .palletBox{
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
> .color_setting_block{
|
||||||
|
margin: auto;
|
||||||
|
background: linear-gradient(70deg, #eee4f3, #f3f4e6);
|
||||||
|
width: 100%;
|
||||||
|
// border-radius: calc(1rem*1.2);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 2px 2px 8px rgba(0,0,0,.3);
|
||||||
|
.vc-chrome{
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
box-shadow:none;
|
||||||
|
}
|
||||||
|
:deep(.chrome_color){
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.vc-chrome-saturation-wrap{
|
||||||
|
width: 30rem;
|
||||||
|
height: 30rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.vc-saturation-pointer{
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.vc-chrome-body{
|
||||||
|
padding: 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 2 auto;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(0,0,0,0);
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
// display: none;
|
||||||
|
.vc-chrome-fields-wrap{
|
||||||
|
margin-top: 5%;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
.vc-chrome-toggle-btn{
|
||||||
|
width: calc(3.2rem*1.2);
|
||||||
|
.vc-chrome-toggle-icon{
|
||||||
|
height: auto;
|
||||||
|
margin-right: calc(-0.4rem*1.2);
|
||||||
|
margin-top: calc(0rem*1.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
svg{
|
||||||
|
width: calc(2.4rem*1.2) !important;
|
||||||
|
height: calc(2.4rem*1.2) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields{
|
||||||
|
.vc-chrome-field{
|
||||||
|
padding-left: calc(.6rem*1.2);
|
||||||
|
}
|
||||||
|
.vc-input__label{
|
||||||
|
font-size: calc(1.6rem*1.2);
|
||||||
|
}
|
||||||
|
.vc-input__input{
|
||||||
|
font-size: 2rem;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-upload-list{
|
||||||
|
|
||||||
|
}
|
||||||
|
.vc-sketch-color-wrap{
|
||||||
|
background-image: url(@/assets/images/homePage/dropper.png);
|
||||||
|
background-size: 3rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
padding: calc(.7rem*1.2);
|
||||||
|
border: 1px solid;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2rem;
|
||||||
|
right: 0rem;
|
||||||
|
border-radius: calc(.5rem*1.2);
|
||||||
|
|
||||||
|
}
|
||||||
|
.vc-chrome-fields{
|
||||||
|
.vc-input__label{
|
||||||
|
margin-top: calc(1rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields:nth-child(2){
|
||||||
|
>:last-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-fields:nth-child(3){
|
||||||
|
>:last-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-controls{
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
.vc-chrome-color-wrap{
|
||||||
|
// width: 3.6rem*1.2);
|
||||||
|
margin-left: calc(2rem*1.2);
|
||||||
|
width: auto;
|
||||||
|
.vc-chrome-active-color{
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.vc-chrome-active-color,.vc-checkerboard{
|
||||||
|
width: calc(3rem*1.2);
|
||||||
|
height: calc(3rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-hue-wrap,.vc-chrome-alpha-wrap{
|
||||||
|
.vc-hue{
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.vc-alpha{
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
height: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
.vc-hue-pointer{
|
||||||
|
transform: translateX(-1.25rem);
|
||||||
|
}
|
||||||
|
.vc-hue-picker{
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(0px,-3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-alpha-wrap{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.vc-chrome-saturation-wrap .vc-saturation-circle{
|
||||||
|
width: calc(1rem*1.2);
|
||||||
|
height: calc(1rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_block{
|
||||||
|
// margin-top: calc(1rem;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: space-between;
|
||||||
|
// font-size: calc(1.6rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 5%;
|
||||||
|
padding-bottom: 5%;
|
||||||
|
margin: calc(0.5rem*1.2) auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
.color_right{
|
||||||
|
width: 13rem;
|
||||||
|
font-size: calc(1.2rem*1.2);
|
||||||
|
color: #666666;
|
||||||
|
.color_rgb_block{
|
||||||
|
display: flex;
|
||||||
|
.rgb_item{
|
||||||
|
margin-left: calc(.2rem*1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_left{
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgb(153, 153, 153);
|
||||||
|
}
|
||||||
|
.color_right,.color_left{
|
||||||
|
>div{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.color_HEX_block,.color_rgb_block{
|
||||||
|
padding: .25rem .6rem;
|
||||||
|
box-shadow: inset 0 0 0 1px #ccc;
|
||||||
|
border-radius: .5rem;
|
||||||
|
justify-content: space-around;
|
||||||
|
text-transform:uppercase;
|
||||||
|
.color_block_bg{
|
||||||
|
width: 1.8rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
// margin-right: .5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_block_bg{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.color_setting_operateSingle{
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
.color_setting_btn{
|
||||||
|
margin: 0 1rem;
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
&.active{
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate{
|
||||||
|
*{
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.color_setting_operate_item{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
.operate_item_box{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_control{
|
||||||
|
.operate_item_delete,.operate_item_angle{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.operate_item_delete{
|
||||||
|
i{
|
||||||
|
display: flex;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.operate_item_angle{
|
||||||
|
.operate_item_angle_box{
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
border: solid 2px #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
>div{
|
||||||
|
height: 100%;
|
||||||
|
width: 1rem;
|
||||||
|
position: relative;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
>div::before{
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
top: 0.2rem;
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_input{
|
||||||
|
width: 80%;
|
||||||
|
// padding: 0 10%;
|
||||||
|
margin: 1.2rem 10%;
|
||||||
|
border-radius: 10%;
|
||||||
|
position: relative;
|
||||||
|
height: 2.5rem;
|
||||||
|
.color_setting_operate_bg{
|
||||||
|
border-radius: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: #fff;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_btn{
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
left: 0;
|
||||||
|
width: 1rem;
|
||||||
|
height: 110%;
|
||||||
|
border: .2rem solid;
|
||||||
|
border-radius: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: content-box;
|
||||||
|
z-index: 2;
|
||||||
|
&.active{
|
||||||
|
border: .3rem solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.color_setting_operate_btn:hover{
|
||||||
|
border: .3rem solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,744 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition name="fade">
|
|
||||||
<div
|
|
||||||
class="select-menu-panel"
|
|
||||||
v-if="visible"
|
|
||||||
:class="{ active: !closePanel }"
|
|
||||||
>
|
|
||||||
<div class="btn" @click="setClosePanel">
|
|
||||||
<i class="fi fi-br-angle-left"></i>
|
|
||||||
</div>
|
|
||||||
<!-- 变换工具顶部 -->
|
|
||||||
<div class="panel-select">
|
|
||||||
<!-- <div class="panel-header">
|
|
||||||
<div class="header-title">变换工具</div>
|
|
||||||
</div> -->
|
|
||||||
<!-- 分割线 -->
|
|
||||||
<!-- <div class="panel-divider"></div> -->
|
|
||||||
<!-- 变换工具内容 -->
|
|
||||||
<div class="tool-content">
|
|
||||||
<div
|
|
||||||
class="object-item"
|
|
||||||
v-for="v in activeObjects"
|
|
||||||
:key="v.id"
|
|
||||||
>
|
|
||||||
<div class="title">{{ v.layer?.name }}</div>
|
|
||||||
<div class="list">
|
|
||||||
<div>
|
|
||||||
<span class="label">W</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="v.width"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="label">H</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="v.height"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- <div>
|
|
||||||
<span class="label">X</span>
|
|
||||||
<input type="number" :value="v.left" disabled />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="label">Y</span>
|
|
||||||
<input type="number" :value="v.top" disabled />
|
|
||||||
</div> -->
|
|
||||||
<div>
|
|
||||||
<span class="label iconfont icon-angle"></span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
:value="Number(Number(v.angle).toFixed(3))"
|
|
||||||
@change="(e) => changeAngle(e, v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickflipHorizontal(v)">
|
|
||||||
<i class="iconfont icon-flip-horizontal"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.flipHorizontal") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickflipVertical(v)">
|
|
||||||
<i class="iconfont icon-flip-vertical"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.flipVertical") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="btn" @click="clickCropImage(v)">
|
|
||||||
<i class="iconfont icon-caijian"></i>
|
|
||||||
<p class="tip">
|
|
||||||
{{ t("Canvas.cropAndAdd") }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import showViewVideo from "@/tool/mount";
|
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { ToolCommand } from "../commands/ToolCommands";
|
|
||||||
import { OperationType } from "../utils/layerHelper";
|
|
||||||
import { loadImageUrlToLayer } from "../utils/imageHelper";
|
|
||||||
import { TransformCommand } from "../commands/StateCommands";
|
|
||||||
const props = defineProps({
|
|
||||||
canvas: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
commandManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
selectManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
layerManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
toolManager: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
activeTool: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const visible = ref(false);
|
|
||||||
//打开隐藏操作面板
|
|
||||||
const closePanel = ref(false);
|
|
||||||
const setClosePanel = () => {
|
|
||||||
closePanel.value = !closePanel.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 国际化
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setupCanvasListeners();
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
removeCanvasListeners();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听 activeTool 变化
|
|
||||||
watch(
|
|
||||||
() => props.activeTool,
|
|
||||||
(newTool) => {
|
|
||||||
if (newTool === OperationType.SELECT) {
|
|
||||||
show();
|
|
||||||
} else {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示面板
|
|
||||||
*/
|
|
||||||
function show() {
|
|
||||||
if (activeObjects.value.length === 0) return;
|
|
||||||
visible.value = true;
|
|
||||||
closePanel.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭面板
|
|
||||||
*/
|
|
||||||
function close() {
|
|
||||||
visible.value = false;
|
|
||||||
}
|
|
||||||
// 获取当前选中的对象
|
|
||||||
const activeObjects = ref([]);
|
|
||||||
const getActiveObject = (e) => {
|
|
||||||
console.log("==========切换激活对象", e);
|
|
||||||
activeObjects.value = e.selected.map((v) => v);
|
|
||||||
activeObjects.value.forEach((v) => {
|
|
||||||
v.layer = props.layerManager.getLayerById(v.layerId);
|
|
||||||
});
|
|
||||||
if (activeObjects.value.length === 0) {
|
|
||||||
close();
|
|
||||||
} else {
|
|
||||||
show(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const lastSelectLayerId = inject("lastSelectLayerId");
|
|
||||||
const layers = inject("layers");
|
|
||||||
const transformObject = (activeObj, initialState, finalState) => {
|
|
||||||
const transformCmd = new TransformCommand({
|
|
||||||
canvas: props.canvas,
|
|
||||||
objectId: activeObj.id,
|
|
||||||
initialState,
|
|
||||||
finalState,
|
|
||||||
objectType: activeObj.type,
|
|
||||||
name: `变换 ${activeObj.type || "对象"}`,
|
|
||||||
layerManager: props.layerManager,
|
|
||||||
layers: layers,
|
|
||||||
lastSelectLayerId: lastSelectLayerId,
|
|
||||||
});
|
|
||||||
props.layerManager.commandManager.execute(transformCmd, {
|
|
||||||
name: "对象修改",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据左上角坐标计算旋转后的新坐标
|
|
||||||
* @param {number} W - 宽度
|
|
||||||
* @param {number} H - 高度
|
|
||||||
* @param {number} currentX - 当前左上角x坐标
|
|
||||||
* @param {number} currentY - 当前左上角y坐标
|
|
||||||
* @param {number} currentAngleDeg - 当前角度(度)
|
|
||||||
* @param {number} newAngleDeg - 新角度(度)
|
|
||||||
* @returns {Object} 旋转后的左上角坐标 {x, y}
|
|
||||||
*/
|
|
||||||
function calculateRotatedTopLeftDeg(
|
|
||||||
W,
|
|
||||||
H,
|
|
||||||
currentX,
|
|
||||||
currentY,
|
|
||||||
currentAngleDeg,
|
|
||||||
newAngleDeg
|
|
||||||
) {
|
|
||||||
const currentAngle = (currentAngleDeg * Math.PI) / 180;
|
|
||||||
const newAngle = (newAngleDeg * Math.PI) / 180;
|
|
||||||
// 1. 用当前角度计算中心点位置
|
|
||||||
const cosCurrent = Math.cos(currentAngle);
|
|
||||||
const sinCurrent = Math.sin(currentAngle);
|
|
||||||
const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent;
|
|
||||||
const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent;
|
|
||||||
|
|
||||||
// 2. 用新角度计算旋转后的左上角位置
|
|
||||||
const cosNew = Math.cos(newAngle);
|
|
||||||
const sinNew = Math.sin(newAngle);
|
|
||||||
const newX = Cx + (-W / 2) * cosNew - (-H / 2) * sinNew;
|
|
||||||
const newY = Cy + (-W / 2) * sinNew + (-H / 2) * cosNew;
|
|
||||||
|
|
||||||
return { x: newX, y: newY };
|
|
||||||
}
|
|
||||||
// 改变角度
|
|
||||||
const changeAngle = (e, obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
const angle = e.target.value;
|
|
||||||
if (obj.originX === "left" && obj.originY === "top") {
|
|
||||||
const width = obj.width * obj.scaleX;
|
|
||||||
const height = obj.height * obj.scaleY;
|
|
||||||
const left = obj.left;
|
|
||||||
const top = obj.top;
|
|
||||||
const { x, y } = calculateRotatedTopLeftDeg(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
obj.angle,
|
|
||||||
angle
|
|
||||||
);
|
|
||||||
finalState.left = x;
|
|
||||||
finalState.top = y;
|
|
||||||
}
|
|
||||||
finalState.angle = angle;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 水平翻转
|
|
||||||
const clickflipHorizontal = (obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
finalState.flipX = !finalState.flipX;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 垂直翻转
|
|
||||||
const clickflipVertical = (obj) => {
|
|
||||||
const initialState = TransformCommand.captureTransformState(obj);
|
|
||||||
const finalState = { ...initialState };
|
|
||||||
finalState.flipY = !finalState.flipY;
|
|
||||||
transformObject(obj, initialState, finalState);
|
|
||||||
};
|
|
||||||
// 裁剪图片
|
|
||||||
const cropImage = inject("cropImage");
|
|
||||||
const clickCropImage = async (obj) => {
|
|
||||||
const base64 = await props.layerManager.getLayerToBase64(obj.layerId);
|
|
||||||
if(base64) cropImage(base64).then((res) => {
|
|
||||||
loadImageUrlToLayer({
|
|
||||||
imageUrl: res,
|
|
||||||
layerManager: props.layerManager,
|
|
||||||
canvas: props.canvas,
|
|
||||||
toolManager: props.toolManager,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateActiveObjects = (arrs, keys) => {
|
|
||||||
arrs.forEach((v) => {
|
|
||||||
activeObjects.value.forEach((item) => {
|
|
||||||
if (item.id === v.id) {
|
|
||||||
keys.forEach((key) => (item[key] = v[key]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
activeObjects.value = [...activeObjects.value];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const objectRotatingChange = (e) => {
|
|
||||||
const arrs = [];
|
|
||||||
if (e.target._objects) {
|
|
||||||
e.target._objects.forEach((v) => arrs.push(v));
|
|
||||||
} else {
|
|
||||||
arrs.push(e.target);
|
|
||||||
}
|
|
||||||
updateActiveObjects(arrs, ["angle"]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置画布事件监听
|
|
||||||
*/
|
|
||||||
function setupCanvasListeners() {
|
|
||||||
if (!props.canvas) return;
|
|
||||||
// 鼠标事件
|
|
||||||
props.canvas.on("selection:created", getActiveObject);
|
|
||||||
props.canvas.on("selection:updated", getActiveObject);
|
|
||||||
props.canvas.on("selection:cleared", close);
|
|
||||||
props.canvas.on("object:rotating", objectRotatingChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除画布事件监听
|
|
||||||
*/
|
|
||||||
function removeCanvasListeners() {
|
|
||||||
if (!props.canvas) return;
|
|
||||||
|
|
||||||
// 移除鼠标事件
|
|
||||||
props.canvas.off("selection:created", getActiveObject);
|
|
||||||
props.canvas.off("selection:updated", getActiveObject);
|
|
||||||
props.canvas.off("selection:cleared", close);
|
|
||||||
props.canvas.off("object:rotating", objectRotatingChange);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.select-menu-panel {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 22px;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
max-width: min(90vw, 640px);
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
user-select: none;
|
|
||||||
&.active {
|
|
||||||
transform: translateY(100%);
|
|
||||||
> .btn {
|
|
||||||
> i {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> .btn {
|
|
||||||
width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
transform: rotate(270deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板和手机适配 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.select-menu-panel {
|
|
||||||
bottom: 15px;
|
|
||||||
left: 15px;
|
|
||||||
right: 15px;
|
|
||||||
max-width: calc(100vw - 30px);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.select-menu-panel {
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
right: 10px;
|
|
||||||
max-width: calc(100vw - 20px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-menu-panel.is-active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-select {
|
|
||||||
// padding: 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板适配 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.panel-header {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机适配 */
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.panel-header {
|
|
||||||
padding: 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn span {
|
|
||||||
margin-top: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn.active {
|
|
||||||
background-color: #007aff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-divider {
|
|
||||||
height: 1px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
margin: 0 10px 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-content {
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 220px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 0 10px;
|
|
||||||
> .object-item {
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
padding: 10px 0;
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
> .title {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
> .list {
|
|
||||||
display: flex;
|
|
||||||
> div {
|
|
||||||
margin-right: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #474747;
|
|
||||||
> .label {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
> input {
|
|
||||||
width: 65px;
|
|
||||||
}
|
|
||||||
.iconfont {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
> div.btn {
|
|
||||||
position: relative;
|
|
||||||
min-width: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
> .tip {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
color: white;
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
margin-left: 0.8rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: none;
|
|
||||||
display: none;
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 97%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5px solid transparent;
|
|
||||||
border-right: 5px solid transparent;
|
|
||||||
border-top: 5px solid rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
|
||||||
> .tip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 平板适配 - 每行4个按钮 */
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.tool-content {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 8px 6px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 手机适配 - 每行3个按钮 */
|
|
||||||
@media screen and (max-width: 480px) {
|
|
||||||
.tool-content {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 6px 4px;
|
|
||||||
padding: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-btn {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 4px;
|
|
||||||
min-width: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
display: flex;
|
|
||||||
// flex-direction: column;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
gap: 4px;
|
|
||||||
.c-svg {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn svg {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-text {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
color: #007aff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框样式 */
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
-webkit-backdrop-filter: blur(5px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-container {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 280px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-dialog-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #666;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather-control {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-control {
|
|
||||||
flex: 1;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-control::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #007aff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feather-value {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 40px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn,
|
|
||||||
.confirm-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-btn {
|
|
||||||
background-color: #007aff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-picker {
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s, transform 0.3s;
|
|
||||||
}
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import i18n from "@/lang/index.ts";
|
||||||
|
const { t } = i18n.global;
|
||||||
|
|
||||||
|
/** 填充重复模式 */
|
||||||
|
export const getSelectOptions = () => ref([
|
||||||
|
{ value: "no-repeat", label: t("Canvas.noRepeat") },
|
||||||
|
{ value: "repeat", label: t("Canvas.repeat") },
|
||||||
|
{ value: "repeat-x", label: t("Canvas.repeatX") },
|
||||||
|
{ value: "repeat-y", label: t("Canvas.repeatY") },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 图层混合模式 */
|
||||||
|
export const getLayerCompositeOptions = () => ref([
|
||||||
|
{ value: "source-over", label: t("Canvas.CompositeNormal"), tip: t("Canvas.CompositeNormalTip") },// 正常
|
||||||
|
{ value: "darken", label: t("Canvas.CompositeDarken"), tip: t("Canvas.CompositeDarkenTip") },// 变暗
|
||||||
|
{ value: "multiply", label: t("Canvas.CompositeMultiply"), tip: t("Canvas.CompositeMultiplyTip") },// 正片叠底
|
||||||
|
{ value: "color-burn", label: t("Canvas.CompositeColorBurn"), tip: t("Canvas.CompositeColorBurnTip") },// 颜色加深
|
||||||
|
|
||||||
|
{ value: "lighten", label: t("Canvas.CompositeLighten"), tip: t("Canvas.CompositeLightenTip") },// 颜色减淡
|
||||||
|
{ value: "screen", label: t("Canvas.CompositeScreen"), tip: t("Canvas.CompositeScreenTip") },// 滤色
|
||||||
|
{ value: "color-dodge", label: t("Canvas.CompositeColorDodge"), tip: t("Canvas.CompositeColorDodgeTip") },// 颜色减淡
|
||||||
|
{ value: "lighter", label: t("Canvas.CompositeLighter"), tip: t("Canvas.CompositeLighterTip") },// 颜色减淡
|
||||||
|
|
||||||
|
{ value: "overlay", label: t("Canvas.CompositeOverlay"), tip: t("Canvas.CompositeOverlayTip") },// 叠加
|
||||||
|
{ value: "soft-light", label: t("Canvas.CompositeSoftLight"), tip: t("Canvas.CompositeSoftLightTip") },// 柔光
|
||||||
|
{ value: "hard-light", label: t("Canvas.CompositeHardLight"), tip: t("Canvas.CompositeHardLightTip") },// 强光
|
||||||
|
|
||||||
|
{ value: "difference", label: t("Canvas.CompositeDifference"), tip: t("Canvas.CompositeDifferenceTip") },// 差值
|
||||||
|
{ value: "exclusion", label: t("Canvas.CompositeExclusion"), tip: t("Canvas.CompositeExclusionTip") },// 排除
|
||||||
|
|
||||||
|
{ value: "hue", label: t("Canvas.CompositeHue"), tip: t("Canvas.CompositeHueTip") },// 色相
|
||||||
|
{ value: "saturation", label: t("Canvas.CompositeSaturation"), tip: t("Canvas.CompositeSaturationTip") },// 饱和度
|
||||||
|
{ value: "color", label: t("Canvas.CompositeColor"), tip: t("Canvas.CompositeColorTip") },// 颜色
|
||||||
|
{ value: "luminosity", label: t("Canvas.CompositeLuminosity"), tip: t("Canvas.CompositeLuminosityTip") },// 亮度
|
||||||
|
|
||||||
|
// { value: "destination-over", label: "背后", tip:"背后:新图形绘制到原内容下方" },
|
||||||
|
// { value: "source-in", label: "颜色加深", tip:"颜色加深:只显示重叠部分,其他透明" },
|
||||||
|
// { value: "destination-in", label: "颜色减淡", tip:"颜色减淡:只显示原内容与新图形重叠部分" },
|
||||||
|
// { value: "source-out", label: "排除", tip:"排除:只显示新图形中不重叠部分" },
|
||||||
|
// { value: "destination-out", label: "差值", tip:"差值:只清除原内容中与新图形重叠部分" },
|
||||||
|
// { value: "xor", label: "排除", tip:"排除:重叠部分透明" },
|
||||||
|
// { value: "copy", label: "正常", tip:"正常:完全忽略原内容,只显示新图形" },
|
||||||
|
// { value: "source-atop", label: "叠加", tip:"叠加:只在与现有内容重叠处绘制新图形" },
|
||||||
|
// { value: "destination-atop", label: "柔光", tip:"柔光:仅保留重叠部分,新图形在原内容后绘制" },
|
||||||
|
// { value: "darker", label: "变暗", tip:"变暗:重叠部分颜色减淡" },
|
||||||
|
]);
|
||||||
@@ -0,0 +1,900 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
class="select-menu-panel"
|
||||||
|
v-if="visible"
|
||||||
|
:class="{ active: !closePanel }"
|
||||||
|
>
|
||||||
|
<div class="btn" @click="setClosePanel">
|
||||||
|
<i class="fi fi-br-angle-left"></i>
|
||||||
|
</div>
|
||||||
|
<!-- 变换工具顶部 -->
|
||||||
|
<div class="panel-select">
|
||||||
|
<!-- <div class="panel-header">
|
||||||
|
<div class="header-title">变换工具</div>
|
||||||
|
</div> -->
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<!-- <div class="panel-divider"></div> -->
|
||||||
|
<!-- 变换工具内容 -->
|
||||||
|
<div class="tool-content">
|
||||||
|
<div
|
||||||
|
class="object-item"
|
||||||
|
v-for="v in activeObjects"
|
||||||
|
:key="v.id"
|
||||||
|
>
|
||||||
|
<div class="title">{{ v.layer?.name }}</div>
|
||||||
|
<div class="list">
|
||||||
|
<div
|
||||||
|
class="input"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<angle-tool
|
||||||
|
:angle="Number(Number(v.angle).toFixed(3))"
|
||||||
|
@input="(e) => inputAngle(e, v)"
|
||||||
|
@change="(e) => changeAngle(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<span class="label"
|
||||||
|
>{{ t("Canvas.opacity") }}:</span
|
||||||
|
>
|
||||||
|
<slider
|
||||||
|
:tipFormatter="
|
||||||
|
(v) => `${Math.round(v * 100)}%`
|
||||||
|
"
|
||||||
|
:value="v.opacity"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
@change="(e) => changeOpacity(e, v)"
|
||||||
|
@input="(e) => inputOpacity(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="btn"
|
||||||
|
@click="clickflipHorizontal(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-flip-horizontal"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.flipHorizontal") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="btn"
|
||||||
|
@click="clickflipVertical(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-flip-vertical"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.flipVertical") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickCropImage(v)"
|
||||||
|
v-if="v.layerId !== SpecialLayerId.COLOR"
|
||||||
|
>
|
||||||
|
<i class="iconfont icon-caijian"></i>
|
||||||
|
<p class="tip">
|
||||||
|
{{ t("Canvas.cropAndAdd") }}
|
||||||
|
</p>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickRasterizeLayer(v)"
|
||||||
|
v-if="v.type !== 'image'"
|
||||||
|
>
|
||||||
|
<span class="label">{{ t("Canvas.RasterizedLayer") }}</span>
|
||||||
|
</div> -->
|
||||||
|
<div class="select">
|
||||||
|
<!-- 混合模式 -->
|
||||||
|
<i class="iconfont icon-hunhemoshi"></i>
|
||||||
|
<my-select
|
||||||
|
:defaultValue="
|
||||||
|
v.layer?.blendMode ||
|
||||||
|
v.globalCompositeOperation
|
||||||
|
"
|
||||||
|
:list="layerCompositeOptions"
|
||||||
|
@change="
|
||||||
|
(n, o) => setLayerComposite(n, o, v, 1)
|
||||||
|
"
|
||||||
|
@active="
|
||||||
|
(n, o) => setLayerComposite(n, o, v, 0)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="btn"
|
||||||
|
@click="clickTest(v)"
|
||||||
|
>
|
||||||
|
<span class="label">测试</span>
|
||||||
|
</div> -->
|
||||||
|
<div
|
||||||
|
class="select"
|
||||||
|
v-if="v.type === 'rect' || v.type === 'image'"
|
||||||
|
>
|
||||||
|
<!-- 平铺 -->
|
||||||
|
<i class="iconfont icon-repeat"></i>
|
||||||
|
<a-select
|
||||||
|
size="small"
|
||||||
|
:defaultValue="
|
||||||
|
typeof v.fill === 'object'
|
||||||
|
? v.fill?.repeat || 'no-repeat'
|
||||||
|
: 'no-repeat'
|
||||||
|
"
|
||||||
|
:options="selectOptions"
|
||||||
|
@change="(e) => changeFillRepeat(e, v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 平铺设置 -->
|
||||||
|
<a-popover
|
||||||
|
v-if="v.type === 'rect'"
|
||||||
|
trigger="click"
|
||||||
|
destroyTooltipOnHide
|
||||||
|
: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>
|
||||||
@@ -412,8 +412,12 @@ const handleToolClick = (tool) => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
.tools-list::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.red-green-mode {
|
.red-green-mode {
|
||||||
background-color: #fff4f4;
|
background-color: #060505;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-indicator {
|
.mode-indicator {
|
||||||
|
|||||||
@@ -270,6 +270,13 @@
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
.layer-color-btn {
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
.layer-actions {
|
.layer-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
|
|||||||
121
src/component/Canvas/CanvasEditor/components/tools/AngleTool.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<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>
|
||||||
160
src/component/Canvas/CanvasEditor/components/tools/Slider.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<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>
|
||||||
@@ -35,7 +35,8 @@ import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
|||||||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||||||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||||||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||||||
import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
|
import PalletPanel from "./components/PalletPanel/index.vue";
|
||||||
|
import SelectMenuPanel from "./components/SelectMenuPanel/index.vue"; // 引入选择工具菜单组件
|
||||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||||
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
||||||
import { ToolManager } from "./managers/ToolManager.js";
|
import { ToolManager } from "./managers/ToolManager.js";
|
||||||
@@ -57,6 +58,7 @@ const emit = defineEmits([
|
|||||||
"changeCanvas", // 画布变更事件
|
"changeCanvas", // 画布变更事件
|
||||||
"canvasInit", // 画布初始化事件
|
"canvasInit", // 画布初始化事件
|
||||||
"trigger-library", // 触发打开Library选择图片事件
|
"trigger-library", // 触发打开Library选择图片事件
|
||||||
|
"before-unmount-export-extra-info", // 组件卸载前导出额外信息事件
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -64,6 +66,10 @@ const props = defineProps({
|
|||||||
type: [Object, String],
|
type: [Object, String],
|
||||||
default: "", // 默认空
|
default: "", // 默认空
|
||||||
},
|
},
|
||||||
|
otherData: {
|
||||||
|
type: [Object, null],
|
||||||
|
default: null, // 默认空对象
|
||||||
|
},
|
||||||
config: {
|
config: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => CanvasConfig, // 默认配置
|
default: () => CanvasConfig, // 默认配置
|
||||||
@@ -78,7 +84,11 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
clothingImageUrl: {
|
clothingImageUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "", // 衣服底图URL
|
default: "", // 衣服底图URL-线稿
|
||||||
|
},
|
||||||
|
clothingImageUrl2: {
|
||||||
|
type: String,
|
||||||
|
default: "", // 衣服底图URL-上色
|
||||||
},
|
},
|
||||||
redGreenImageUrl: {
|
redGreenImageUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -250,6 +260,7 @@ onMounted(async () => {
|
|||||||
canvasColor,
|
canvasColor,
|
||||||
enabledRedGreenMode: props.enabledRedGreenMode,
|
enabledRedGreenMode: props.enabledRedGreenMode,
|
||||||
isFixedErasable: props.isFixedErasable,
|
isFixedErasable: props.isFixedErasable,
|
||||||
|
props,
|
||||||
});
|
});
|
||||||
canvasManager.canvas.activeLayerId = activeLayerId;
|
canvasManager.canvas.activeLayerId = activeLayerId;
|
||||||
canvasManager.activeLayerId = activeLayerId;
|
canvasManager.activeLayerId = activeLayerId;
|
||||||
@@ -307,6 +318,7 @@ onMounted(async () => {
|
|||||||
canvas: canvasManager.canvas,
|
canvas: canvasManager.canvas,
|
||||||
commandManager,
|
commandManager,
|
||||||
layerManager,
|
layerManager,
|
||||||
|
canvasManager,
|
||||||
toolManager,
|
toolManager,
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
pasteText: (text) => {
|
pasteText: (text) => {
|
||||||
@@ -435,6 +447,12 @@ onMounted(async () => {
|
|||||||
canvasManager.canvas.width,
|
canvasManager.canvas.width,
|
||||||
canvasManager.canvas.height
|
canvasManager.canvas.height
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(props.otherData && !props.otherData.canvasId) {
|
||||||
|
await canvasManager?.createOtherLayers(props.otherData);
|
||||||
|
await layerManager?.layerSort?.rearrangeObjects();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 设置固定图层是否可擦除
|
// // 设置固定图层是否可擦除
|
||||||
@@ -527,12 +545,15 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(async () => {
|
||||||
// if (import.meta.hot) {
|
// if (import.meta.hot) {
|
||||||
// // 热更新 ?
|
// // 热更新 ?
|
||||||
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
||||||
// return; // 开发环境下不卸载组件
|
// return; // 开发环境下不卸载组件
|
||||||
// }
|
// }
|
||||||
|
const extraInfo = await canvasManager.exportExtraInfo();
|
||||||
|
emit("before-unmount-export-extra-info", extraInfo);
|
||||||
|
|
||||||
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
||||||
canvasManager?.dispose?.();
|
canvasManager?.dispose?.();
|
||||||
commandManager?.dispose?.();
|
commandManager?.dispose?.();
|
||||||
@@ -720,42 +741,7 @@ function deleteFun() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeLayer(layerId) {
|
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);
|
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() {
|
function triggerImageUpload() {
|
||||||
@@ -902,13 +888,18 @@ const changeCanvas = async (command) => {
|
|||||||
...command, // 传递完整的命令数据
|
...command, // 传递完整的命令数据
|
||||||
};
|
};
|
||||||
emit("changeCanvas", commandData);
|
emit("changeCanvas", commandData);
|
||||||
if (command.canUndo || command.canRedo) {
|
canvasManager.changeCanvas(commandData);
|
||||||
|
if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const imageData = await canvasManager.exportImage({
|
try {
|
||||||
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
const imageData = await canvasManager.exportImage({
|
||||||
isCropByBg: true,
|
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
|
||||||
});
|
isCropByBg: true,
|
||||||
emit("trigger-red-green-mouseup", imageData);
|
});
|
||||||
|
emit("trigger-red-green-mouseup", imageData);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -918,6 +909,14 @@ const cropImage = (url) => {
|
|||||||
return cropImageRef.value.open(url)
|
return cropImageRef.value.open(url)
|
||||||
};
|
};
|
||||||
provide("cropImage", cropImage); // 提供给子组件使用
|
provide("cropImage", cropImage); // 提供给子组件使用
|
||||||
|
// 颜色选择器组件
|
||||||
|
const palletPanelRef = ref(null);
|
||||||
|
const palletPanel = (url) => {
|
||||||
|
return palletPanelRef.value.open(url)
|
||||||
|
};
|
||||||
|
provide("palletPanel", palletPanel); // 提供给子组件使用
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理画布容器的拖放事件
|
// 处理画布容器的拖放事件
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false);
|
||||||
@@ -1014,6 +1013,7 @@ defineExpose({
|
|||||||
exportImage: ({
|
exportImage: ({
|
||||||
isContainBg = false, // 是否包含背景图层
|
isContainBg = false, // 是否包含背景图层
|
||||||
isContainFixed = false, // 是否包含固定图层
|
isContainFixed = false, // 是否包含固定图层
|
||||||
|
isContainFixedOther = false, // 是否包含其他固定图层
|
||||||
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小
|
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小
|
||||||
layerId = "", // 导出具体图层ID
|
layerId = "", // 导出具体图层ID
|
||||||
layerIdArray = [], // 导出多个图层ID数组
|
layerIdArray = [], // 导出多个图层ID数组
|
||||||
@@ -1023,6 +1023,7 @@ defineExpose({
|
|||||||
return canvasManager.exportImage({
|
return canvasManager.exportImage({
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
layerId,
|
layerId,
|
||||||
layerIdArray,
|
layerIdArray,
|
||||||
@@ -1030,6 +1031,10 @@ defineExpose({
|
|||||||
isEnhanceImg,
|
isEnhanceImg,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// 导出颜色图层
|
||||||
|
exportColorLayer: () => {
|
||||||
|
return canvasManager.exportColorLayer();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 移动图层位置
|
* 移动图层位置
|
||||||
* @param {string} layerId 图层ID
|
* @param {string} layerId 图层ID
|
||||||
@@ -1048,6 +1053,14 @@ defineExpose({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有信息
|
||||||
|
* @returns {Object} 包含所有图层信息的对象
|
||||||
|
*/
|
||||||
|
exportExtraInfo: () => {
|
||||||
|
return canvasManager.exportExtraInfo();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拖拽排序图层
|
* 拖拽排序图层
|
||||||
* @param {number} oldIndex 原索引
|
* @param {number} oldIndex 原索引
|
||||||
@@ -1245,6 +1258,7 @@ defineExpose({
|
|||||||
:commandManager="commandManager"
|
:commandManager="commandManager"
|
||||||
:selectionManager="selectionManager"
|
:selectionManager="selectionManager"
|
||||||
:layerManager="layerManager"
|
:layerManager="layerManager"
|
||||||
|
:canvasManager="canvasManager"
|
||||||
:toolManager="toolManager"
|
:toolManager="toolManager"
|
||||||
:activeTool="activeTool"
|
:activeTool="activeTool"
|
||||||
/>
|
/>
|
||||||
@@ -1269,6 +1283,7 @@ defineExpose({
|
|||||||
?
|
?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图层面板组件 -->
|
<!-- 图层面板组件 -->
|
||||||
@@ -1298,9 +1313,11 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- 裁剪图片组件 -->
|
|
||||||
<CropImage ref="cropImageRef" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 裁剪图片组件 -->
|
||||||
|
<CropImage ref="cropImageRef" />
|
||||||
|
<!-- 颜色选择器组件 -->
|
||||||
|
<PalletPanel ref="palletPanelRef" />
|
||||||
|
|
||||||
<!-- <div class="footer-actions">
|
<!-- <div class="footer-actions">
|
||||||
<button class="share-btn">Share</button>
|
<button class="share-btn">Share</button>
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import {
|
|||||||
isGroupLayer,
|
isGroupLayer,
|
||||||
OperationType,
|
OperationType,
|
||||||
OperationTypes,
|
OperationTypes,
|
||||||
|
findLayer,
|
||||||
|
createLayer,
|
||||||
|
LayerType,
|
||||||
|
SpecialLayerId,
|
||||||
|
BlendMode,
|
||||||
} from "../utils/layerHelper";
|
} from "../utils/layerHelper";
|
||||||
|
import { ObjectMoveCommand } from "../commands/ObjectCommands";
|
||||||
import { AnimationManager } from "./animation/AnimationManager";
|
import { AnimationManager } from "./animation/AnimationManager";
|
||||||
import { createCanvas } from "../utils/canvasFactory";
|
import { createCanvas } from "../utils/canvasFactory";
|
||||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||||
@@ -21,6 +27,13 @@ import {
|
|||||||
findObjectById,
|
findObjectById,
|
||||||
generateId,
|
generateId,
|
||||||
optimizeCanvasRendering,
|
optimizeCanvasRendering,
|
||||||
|
palletToFill,
|
||||||
|
fillToCssStyle,
|
||||||
|
calculateRotatedTopLeftDeg,
|
||||||
|
calculateCenterPoint,
|
||||||
|
createPatternTransform,
|
||||||
|
getTransformScaleAngle,
|
||||||
|
base64ToCanvas,
|
||||||
} from "../utils/helper";
|
} from "../utils/helper";
|
||||||
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
||||||
import { isFunction } from "lodash-es";
|
import { isFunction } from "lodash-es";
|
||||||
@@ -30,6 +43,11 @@ import {
|
|||||||
validateLayerAssociations,
|
validateLayerAssociations,
|
||||||
} from "../utils/layerUtils";
|
} from "../utils/layerUtils";
|
||||||
import { imageModeHandler } from "../utils/imageHelper";
|
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 {
|
export class CanvasManager {
|
||||||
constructor(canvasElement, options) {
|
constructor(canvasElement, options) {
|
||||||
@@ -50,6 +68,7 @@ export class CanvasManager {
|
|||||||
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
|
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
|
||||||
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
||||||
this.handleCanvasInit = null; // 画布初始化回调函数
|
this.handleCanvasInit = null; // 画布初始化回调函数
|
||||||
|
this.props = options.props || {};
|
||||||
// 初始化画布
|
// 初始化画布
|
||||||
this.initializeCanvas();
|
this.initializeCanvas();
|
||||||
}
|
}
|
||||||
@@ -83,10 +102,10 @@ export class CanvasManager {
|
|||||||
|
|
||||||
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
|
||||||
|
|
||||||
// // 设置画布辅助线
|
// 设置画布辅助线
|
||||||
// initAligningGuidelines(this.canvas);
|
initAligningGuidelines(this.canvas);
|
||||||
|
|
||||||
// // 设置画布中心线
|
// 设置画布中心线
|
||||||
// initCenteringGuidelines(this.canvas);
|
// initCenteringGuidelines(this.canvas);
|
||||||
|
|
||||||
// 初始化画布事件监听器
|
// 初始化画布事件监听器
|
||||||
@@ -431,7 +450,7 @@ export class CanvasManager {
|
|||||||
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
||||||
* 这样可以保持对象间的相对位置关系不变
|
* 这样可以保持对象间的相对位置关系不变
|
||||||
*/
|
*/
|
||||||
centerAllObjects() {
|
async centerAllObjects() {
|
||||||
if (!this.canvas) return;
|
if (!this.canvas) return;
|
||||||
|
|
||||||
// 获取所有可见对象(不是背景元素的对象)
|
// 获取所有可见对象(不是背景元素的对象)
|
||||||
@@ -448,8 +467,8 @@ export class CanvasManager {
|
|||||||
// 获取背景对象
|
// 获取背景对象
|
||||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||||
|
|
||||||
!this.canvas?.clipPath &&
|
// !this.canvas?.clipPath &&
|
||||||
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
// this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
this.canvas?.clipPath?.set?.({
|
this.canvas?.clipPath?.set?.({
|
||||||
left: this.width / 2,
|
left: this.width / 2,
|
||||||
@@ -496,7 +515,6 @@ export class CanvasManager {
|
|||||||
// 计算背景层的偏移量
|
// 计算背景层的偏移量
|
||||||
const deltaX = backgroundObject.left - backgroundOldLeft;
|
const deltaX = backgroundObject.left - backgroundOldLeft;
|
||||||
const deltaY = backgroundObject.top - backgroundOldTop;
|
const deltaY = backgroundObject.top - backgroundOldTop;
|
||||||
|
|
||||||
// 将相同的偏移量应用到所有其他对象上
|
// 将相同的偏移量应用到所有其他对象上
|
||||||
const otherObjects = visibleObjects.filter(
|
const otherObjects = visibleObjects.filter(
|
||||||
(obj) => obj !== backgroundObject
|
(obj) => obj !== backgroundObject
|
||||||
@@ -549,8 +567,20 @@ export class CanvasManager {
|
|||||||
this.updateMaskPosition(backgroundObject);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -600,7 +630,7 @@ export class CanvasManager {
|
|||||||
* @param {Number} canvasWidth 画布宽度
|
* @param {Number} canvasWidth 画布宽度
|
||||||
* @param {Number} canvasHeight 画布高度
|
* @param {Number} canvasHeight 画布高度
|
||||||
*/
|
*/
|
||||||
centerBackgroundLayer(canvasWidth, canvasHeight) {
|
async centerBackgroundLayer(canvasWidth, canvasHeight) {
|
||||||
const backgroundLayerObject = this.getBackgroundLayer();
|
const backgroundLayerObject = this.getBackgroundLayer();
|
||||||
if (!backgroundLayerObject) return false;
|
if (!backgroundLayerObject) return false;
|
||||||
|
|
||||||
@@ -646,6 +676,11 @@ export class CanvasManager {
|
|||||||
if (this.maskLayer) {
|
if (this.maskLayer) {
|
||||||
this.canvas.remove(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({
|
this.maskLayer = new fabric.Rect({
|
||||||
@@ -706,6 +741,82 @@ export class CanvasManager {
|
|||||||
|
|
||||||
return backgroundLayerByBgLayer;
|
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 背景层对象
|
* @param {Object} backgroundLayerObject 背景层对象
|
||||||
@@ -798,7 +909,7 @@ export class CanvasManager {
|
|||||||
|
|
||||||
// 如果找到了图层,则生成缩略图
|
// 如果找到了图层,则生成缩略图
|
||||||
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
|
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
|
||||||
|
this.layerManager?.sortLayers?.();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,11 +918,13 @@ export class CanvasManager {
|
|||||||
* @param {Object} options 导出选项
|
* @param {Object} options 导出选项
|
||||||
* @param {Boolean} options.isContainBg 是否包含背景图层
|
* @param {Boolean} options.isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
||||||
|
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
|
||||||
* @param {String} options.layerId 导出具体图层ID
|
* @param {String} options.layerId 导出具体图层ID
|
||||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||||
|
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||||
* @returns {String} 导出的图片数据URL
|
* @returns {String} 导出的图片数据URL
|
||||||
*/
|
*/
|
||||||
async exportImage(options = {}) {
|
async exportImage(options = {}) {
|
||||||
@@ -832,6 +945,7 @@ export class CanvasManager {
|
|||||||
options.restoreOpacityInRedGreen !== undefined
|
options.restoreOpacityInRedGreen !== undefined
|
||||||
? options.restoreOpacityInRedGreen
|
? options.restoreOpacityInRedGreen
|
||||||
: false, // 默认在红绿图模式下恢复透明度
|
: false, // 默认在红绿图模式下恢复透明度
|
||||||
|
excludedLayers: [SpecialLayerId.SPECIAL_GROUP],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
||||||
@@ -846,7 +960,7 @@ export class CanvasManager {
|
|||||||
const normalLayerIds =
|
const normalLayerIds =
|
||||||
this.layers?.value
|
this.layers?.value
|
||||||
?.filter(
|
?.filter(
|
||||||
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
|
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther && layer.visible
|
||||||
)
|
)
|
||||||
?.map((layer) => layer.id) || [];
|
?.map((layer) => layer.id) || [];
|
||||||
|
|
||||||
@@ -857,11 +971,143 @@ export class CanvasManager {
|
|||||||
}
|
}
|
||||||
return await this.exportManager.exportImage(enhancedOptions);
|
return await this.exportManager.exportImage(enhancedOptions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("CanvasManager导出图片失败:", error);
|
console.warn("CanvasManager导出图片失败:", error);
|
||||||
throw 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,
|
||||||
|
gap: [0, 0],
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
obj.gap = [fill_.gapX, fill_.gapY];
|
||||||
|
}
|
||||||
|
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() {
|
dispose() {
|
||||||
// 释放导出管理器资源
|
// 释放导出管理器资源
|
||||||
if (this.exportManager) {
|
if (this.exportManager) {
|
||||||
@@ -956,38 +1202,58 @@ export class CanvasManager {
|
|||||||
// };
|
// };
|
||||||
try {
|
try {
|
||||||
// 清除画布中选中状态
|
// 清除画布中选中状态
|
||||||
this.canvas.discardActiveObject();
|
// this.canvas.discardActiveObject();
|
||||||
this.canvas.renderAll();
|
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(
|
const simplifyLayersData = simplifyLayers(
|
||||||
JSON.parse(JSON.stringify(this.layers.value))
|
JSON.parse(JSON.stringify(this.layers.value)),
|
||||||
|
excludedLayers
|
||||||
);
|
);
|
||||||
console.log("获取画布JSON数据...", simplifyLayersData);
|
const data = {
|
||||||
return JSON.stringify({
|
canvas,
|
||||||
canvas: this.canvas.toJSON([
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"layerId",
|
|
||||||
"layerName",
|
|
||||||
"isBackground",
|
|
||||||
"isLocked",
|
|
||||||
"isVisible",
|
|
||||||
"isFixed",
|
|
||||||
"parentId",
|
|
||||||
"eraser",
|
|
||||||
"eraserable",
|
|
||||||
"erasable",
|
|
||||||
"customType",
|
|
||||||
]),
|
|
||||||
layers: simplifyLayersData, // 简化图层数据
|
layers: simplifyLayersData, // 简化图层数据
|
||||||
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
|
|
||||||
version: "1.0", // 添加版本信息
|
version: "1.0", // 添加版本信息
|
||||||
timestamp: new Date().toISOString(), // 添加时间戳
|
timestamp: new Date().toISOString(), // 添加时间戳
|
||||||
canvasWidth: this.canvasWidth.value,
|
canvasWidth: this.canvasWidth.value,
|
||||||
canvasHeight: this.canvasHeight.value,
|
canvasHeight: this.canvasHeight.value,
|
||||||
canvasColor: this.canvasColor.value,
|
canvasColor: this.canvasColor.value,
|
||||||
activeLayerId: this.layerManager?.activeLayerId?.value,
|
activeLayerId: this.layerManager?.activeLayerId?.value,
|
||||||
});
|
};
|
||||||
|
console.log("获取画布JSON数据...", data);
|
||||||
|
return JSON.stringify(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取画布JSON失败:", error);
|
console.error("获取画布JSON失败:", error);
|
||||||
throw new Error("获取画布JSON失败");
|
throw new Error("获取画布JSON失败");
|
||||||
@@ -1070,8 +1336,10 @@ export class CanvasManager {
|
|||||||
// }
|
// }
|
||||||
try {
|
try {
|
||||||
// 重置画布数据
|
// 重置画布数据
|
||||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
await this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||||
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
|
||||||
|
await this.createOtherLayers(this.props.otherData);
|
||||||
|
|
||||||
// 重新构建对象关系
|
// 重新构建对象关系
|
||||||
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
|
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
|
||||||
// 验证图层关联关系 - 稳定后可以注释
|
// 验证图层关联关系 - 稳定后可以注释
|
||||||
@@ -1099,9 +1367,7 @@ export class CanvasManager {
|
|||||||
await calllBack?.();
|
await calllBack?.();
|
||||||
|
|
||||||
// 确保所有对象的交互性正确设置
|
// 确保所有对象的交互性正确设置
|
||||||
await this.layerManager?.updateLayersObjectsInteractivity?.(
|
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||||
false
|
|
||||||
);
|
|
||||||
console.log(this.layerManager.layers.value);
|
console.log(this.layerManager.layers.value);
|
||||||
|
|
||||||
// 更新所有缩略图
|
// 更新所有缩略图
|
||||||
@@ -1126,6 +1392,314 @@ 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();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 缩放红绿图模式内容以适应当前画布大小
|
* 缩放红绿图模式内容以适应当前画布大小
|
||||||
* 确保衣服底图和红绿图永远在画布内可见
|
* 确保衣服底图和红绿图永远在画布内可见
|
||||||
@@ -1249,6 +1823,7 @@ export class CanvasManager {
|
|||||||
return fixedLayer.fabricObject || null;
|
return fixedLayer.fabricObject || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有普通图层对象(包括红绿图)
|
* 获取所有普通图层对象(包括红绿图)
|
||||||
* @returns {Array} 普通图层对象数组
|
* @returns {Array} 普通图层对象数组
|
||||||
@@ -1315,4 +1890,46 @@ export class CanvasManager {
|
|||||||
|
|
||||||
return sizeMatch && positionMatch;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { findObjectById } from "../utils/helper";
|
import { findObjectById } from "../utils/helper";
|
||||||
import { createRasterizedImage } from "../utils/selectionToImage";
|
import { createRasterizedImage } from "../utils/selectionToImage";
|
||||||
|
import { OperationType, SpecialLayerId } from "../utils/layerHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片导出管理器
|
* 图片导出管理器
|
||||||
@@ -18,26 +19,39 @@ export class ExportManager {
|
|||||||
* @param {Object} options 导出选项
|
* @param {Object} options 导出选项
|
||||||
* @param {Boolean} options.isContainBg 是否包含背景图层
|
* @param {Boolean} options.isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
* @param {Boolean} options.isContainFixed 是否包含固定图层
|
||||||
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
|
||||||
|
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
|
||||||
* @param {String} options.layerId 导出具体图层ID
|
* @param {String} options.layerId 导出具体图层ID
|
||||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||||
|
* @param {Array} options.layerIdArray2 导出多个图层ID数组2
|
||||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||||
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
* @param {Boolean} options.isEnhanceImg 是否是增强图片
|
||||||
|
* @param {Array} options.excludedLayers 排除的图层ID数组
|
||||||
* @returns {String} 导出的图片数据URL
|
* @returns {String} 导出的图片数据URL
|
||||||
*/
|
*/
|
||||||
exportImage(options = {}) {
|
async exportImage(options = {}) {
|
||||||
const {
|
const {
|
||||||
isContainBg = false,
|
isContainBg = false,
|
||||||
isContainFixed = false,
|
isContainFixed = false,
|
||||||
|
isContainFixedOther = false, // 是否包含其他固定图层
|
||||||
isCropByBg = false, // 是否使用背景大小裁剪
|
isCropByBg = false, // 是否使用背景大小裁剪
|
||||||
layerId = "",
|
layerId = "",
|
||||||
layerIdArray = [],
|
layerIdArray = [],
|
||||||
|
layerIdArray2 = null,
|
||||||
expPicType = "png",
|
expPicType = "png",
|
||||||
restoreOpacityInRedGreen = true,
|
restoreOpacityInRedGreen = true,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
|
excludedLayers = [], // 排除的图层ID数组
|
||||||
} = options;
|
} = options;
|
||||||
try {
|
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;
|
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
||||||
// 如果指定了具体图层ID,导出指定图层
|
// 如果指定了具体图层ID,导出指定图层
|
||||||
@@ -48,7 +62,7 @@ export class ExportManager {
|
|||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +73,11 @@ export class ExportManager {
|
|||||||
expPicType,
|
expPicType,
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +86,13 @@ export class ExportManager {
|
|||||||
expPicType,
|
expPicType,
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg,
|
isCropByBg,
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
|
layerIdArray2,
|
||||||
|
excludedLayers, // 排除的图层ID数组
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("导出图片失败:", error);
|
console.error("导出图片失败:", error);
|
||||||
@@ -128,8 +146,6 @@ export class ExportManager {
|
|||||||
objectsToExport,
|
objectsToExport,
|
||||||
expPicType,
|
expPicType,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg, // 是否使用背景大小裁剪
|
|
||||||
isEnhanceImg, // 是否是增强图片
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +165,7 @@ export class ExportManager {
|
|||||||
* @param {String} expPicType 导出类型
|
* @param {String} expPicType 导出类型
|
||||||
* @param {Boolean} isContainBg 是否包含背景图层
|
* @param {Boolean} isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||||
|
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||||
@@ -161,6 +178,7 @@ export class ExportManager {
|
|||||||
expPicType,
|
expPicType,
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg, // 是否使用背景大小裁剪
|
isCropByBg, // 是否使用背景大小裁剪
|
||||||
@@ -174,7 +192,8 @@ export class ExportManager {
|
|||||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||||
layerIdArray,
|
layerIdArray,
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
);
|
);
|
||||||
|
|
||||||
if (objectsToExport.length === 0) {
|
if (objectsToExport.length === 0) {
|
||||||
@@ -206,10 +225,12 @@ export class ExportManager {
|
|||||||
* @param {String} expPicType 导出类型
|
* @param {String} expPicType 导出类型
|
||||||
* @param {Boolean} isContainBg 是否包含背景图层
|
* @param {Boolean} isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||||
|
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||||
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
* @param {Boolean} isRedGreenMode 是否为红绿图模式
|
||||||
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
* @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||||
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
* @param {Boolean} isCropByBg 是否使用背景大小裁剪
|
||||||
* @param {Boolean} isEnhanceImg 是否是增强图片
|
* @param {Boolean} isEnhanceImg 是否是增强图片
|
||||||
|
* @param {Array} layerIdArray 导出多个图层ID数组2
|
||||||
* @returns {String} 图片数据URL
|
* @returns {String} 图片数据URL
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@@ -217,16 +238,21 @@ export class ExportManager {
|
|||||||
expPicType,
|
expPicType,
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
isRedGreenMode,
|
isRedGreenMode,
|
||||||
restoreOpacityInRedGreen,
|
restoreOpacityInRedGreen,
|
||||||
isCropByBg, // 是否使用背景大小裁剪
|
isCropByBg, // 是否使用背景大小裁剪
|
||||||
isEnhanceImg, // 是否是增强图片
|
isEnhanceImg, // 是否是增强图片
|
||||||
|
layerIdArray, // 导出所有图层
|
||||||
|
excludedLayers, // 排除的图层ID数组
|
||||||
) {
|
) {
|
||||||
// 按图层顺序收集对象(从底到顶)
|
// 按图层顺序收集对象(从底到顶)
|
||||||
const objectsToExport = this._collectObjectsByLayerOrder(
|
const objectsToExport = this._collectObjectsByLayerOrder(
|
||||||
null, // 导出所有图层
|
layerIdArray, // 导出所有图层
|
||||||
isContainBg,
|
isContainBg,
|
||||||
isContainFixed,
|
isContainFixed,
|
||||||
|
isContainFixedOther, // 是否包含其他固定图层
|
||||||
|
excludedLayers,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (objectsToExport.length === 0) {
|
if (objectsToExport.length === 0) {
|
||||||
@@ -282,10 +308,11 @@ export class ExportManager {
|
|||||||
/**
|
/**
|
||||||
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象)
|
* 从图层收集对象(优化版本 - 通过ID查找画布中的真实对象)
|
||||||
* @param {Object} layer 图层对象
|
* @param {Object} layer 图层对象
|
||||||
|
* @param {Boolean} isChildren 是否递归收集子图层的对象
|
||||||
* @returns {Array} 画布中的真实对象数组
|
* @returns {Array} 画布中的真实对象数组
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_collectObjectsFromLayer(layer) {
|
_collectObjectsFromLayer(layer, isChildren = true) {
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -314,10 +341,10 @@ export class ExportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 递归收集子图层的对象
|
// 递归收集子图层的对象
|
||||||
if (layer.children && layer.children.length > 0) {
|
if (isChildren && layer.children && layer.children.length > 0) {
|
||||||
for (let i = layer.children.length - 1; i >= 0; i--) {
|
for (let i = layer.children.length - 1; i >= 0; i--) {
|
||||||
const childLayer = layer.children[i];
|
const childLayer = layer.children[i];
|
||||||
const childObjects = this._collectObjectsFromLayer(childLayer);
|
const childObjects = this._collectObjectsFromLayer(childLayer, isChildren);
|
||||||
realObjects.push(...childObjects);
|
realObjects.push(...childObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,12 +410,14 @@ export class ExportManager {
|
|||||||
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
* @param {Array|null} layerIdArray 图层ID数组,null表示所有图层
|
||||||
* @param {Boolean} isContainBg 是否包含背景图层
|
* @param {Boolean} isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||||
|
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||||
|
* @param {Array} excludedLayers 排除的图层ID数组
|
||||||
* @returns {Array} 按正确顺序排列的真实对象数组
|
* @returns {Array} 按正确顺序排列的真实对象数组
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) {
|
_collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther, excludedLayers) {
|
||||||
const objectsToExport = [];
|
const objectsToExport = [];
|
||||||
const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表
|
const allLayers = this._getAllLayersFlattened(excludedLayers); // 获取扁平化的图层列表
|
||||||
|
|
||||||
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
// 图层数组是从顶到底的顺序,需要反向遍历以获得从底到顶的渲染顺序
|
||||||
for (let i = allLayers.length - 1; i >= 0; i--) {
|
for (let i = allLayers.length - 1; i >= 0; i--) {
|
||||||
@@ -398,11 +427,11 @@ export class ExportManager {
|
|||||||
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
|
if (layerIdArray && !layerIdArray.includes(layer.id)) continue;
|
||||||
|
|
||||||
// 检查图层类型过滤条件
|
// 检查图层类型过滤条件
|
||||||
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
|
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (layer.visible) {
|
if (layer.visible) {
|
||||||
const layerObjects = this._collectObjectsFromLayer(layer);
|
const layerObjects = this._collectObjectsFromLayer(layer, false);
|
||||||
objectsToExport.push(...layerObjects);
|
objectsToExport.push(...layerObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,15 +440,19 @@ export class ExportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取扁平化的图层列表(包含子图层)
|
* 获取扁平化的图层列表(包含子图层),排除指定的图层
|
||||||
|
* @param {Array} excludedLayers 排除的图层ID数组
|
||||||
* @returns {Array} 扁平化的图层数组
|
* @returns {Array} 扁平化的图层数组
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getAllLayersFlattened() {
|
_getAllLayersFlattened(excludedLayers) {
|
||||||
const flattenedLayers = [];
|
const flattenedLayers = [];
|
||||||
const rootLayers = this._getAllLayers();
|
const rootLayers = this._getAllLayers();
|
||||||
|
|
||||||
const flattenLayer = (layer) => {
|
const flattenLayer = (layer) => {
|
||||||
|
// 检查是否在排除列表中
|
||||||
|
if (excludedLayers && excludedLayers.includes(layer.id)) return;
|
||||||
|
|
||||||
flattenedLayers.push(layer);
|
flattenedLayers.push(layer);
|
||||||
|
|
||||||
// 递归处理子图层
|
// 递归处理子图层
|
||||||
@@ -434,7 +467,6 @@ export class ExportManager {
|
|||||||
for (const layer of rootLayers) {
|
for (const layer of rootLayers) {
|
||||||
flattenLayer(layer);
|
flattenLayer(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return flattenedLayers;
|
return flattenedLayers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,37 +587,22 @@ export class ExportManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
|
|
||||||
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
|
|
||||||
|
|
||||||
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
// 使用固定图层的实际显示尺寸作为导出画布尺寸
|
||||||
const canvasWidth = Math.round(fixedBounds.width);
|
const canvasWidth = (fixedLayerObject.width);
|
||||||
const canvasHeight = Math.round(fixedBounds.height);
|
const canvasHeight = (fixedLayerObject.height);
|
||||||
|
|
||||||
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
||||||
console.log("固定图层边界:", fixedBounds);
|
const tempFabricCanvas = new fabric.StaticCanvas()
|
||||||
|
tempFabricCanvas.setDimensions({
|
||||||
// 创建固定尺寸的临时画布
|
|
||||||
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,
|
width: canvasWidth,
|
||||||
height: canvasHeight,
|
height: canvasHeight,
|
||||||
backgroundColor: null,
|
backgroundColor: null,
|
||||||
enableRetinaScaling: true,
|
// enableRetinaScaling: true,
|
||||||
imageSmoothingEnabled: true,
|
imageSmoothingEnabled: true,
|
||||||
});
|
});
|
||||||
tempFabricCanvas.setZoom(1);
|
// tempFabricCanvas.setZoom(1);
|
||||||
|
console.log("==========", fixedLayerObject)
|
||||||
try {
|
try {
|
||||||
// 获取裁剪路径对象(如果存在)
|
|
||||||
const clipPathObject = await this._getClipPathObject(fixedBounds);
|
|
||||||
|
|
||||||
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
|
||||||
for (let i = 0; i < objectsToExport.length; i++) {
|
for (let i = 0; i < objectsToExport.length; i++) {
|
||||||
const obj = objectsToExport[i];
|
const obj = objectsToExport[i];
|
||||||
@@ -594,20 +611,17 @@ export class ExportManager {
|
|||||||
restoreOpacityInRedGreen && true
|
restoreOpacityInRedGreen && true
|
||||||
);
|
);
|
||||||
if (cloned) {
|
if (cloned) {
|
||||||
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
|
|
||||||
cloned.set({
|
cloned.set({
|
||||||
left: cloned.left - fixedBounds.left,
|
left: canvasWidth / 2,
|
||||||
top: cloned.top - fixedBounds.top,
|
top: canvasHeight / 2,
|
||||||
|
scaleX: cloned.scaleX / fixedLayerObject.scaleX,
|
||||||
|
scaleY: cloned.scaleY / fixedLayerObject.scaleY,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
});
|
});
|
||||||
|
console.log("==========", {...cloned})
|
||||||
// 更新对象坐标
|
// 更新对象坐标
|
||||||
cloned.setCoords();
|
cloned.setCoords();
|
||||||
|
|
||||||
// 设置裁剪路径到对象
|
|
||||||
if (clipPathObject) {
|
|
||||||
cloned.clipPath = clipPathObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFabricCanvas.add(cloned);
|
tempFabricCanvas.add(cloned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,7 +630,7 @@ export class ExportManager {
|
|||||||
tempFabricCanvas.renderAll();
|
tempFabricCanvas.renderAll();
|
||||||
|
|
||||||
// 生成图片
|
// 生成图片
|
||||||
return this._generateHighQualityDataURL(tempCanvas, expPicType);
|
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
|
||||||
} finally {
|
} finally {
|
||||||
this._cleanupTempCanvas(tempFabricCanvas);
|
this._cleanupTempCanvas(tempFabricCanvas);
|
||||||
}
|
}
|
||||||
@@ -736,7 +750,7 @@ export class ExportManager {
|
|||||||
*/
|
*/
|
||||||
_cloneObjectAsync(
|
_cloneObjectAsync(
|
||||||
obj,
|
obj,
|
||||||
propertiesToInclude = ["id", "layerId", "layerName", "name"]
|
propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"]
|
||||||
) {
|
) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
@@ -1031,10 +1045,11 @@ export class ExportManager {
|
|||||||
* @param {Object} layer 图层对象
|
* @param {Object} layer 图层对象
|
||||||
* @param {Boolean} isContainBg 是否包含背景图层
|
* @param {Boolean} isContainBg 是否包含背景图层
|
||||||
* @param {Boolean} isContainFixed 是否包含固定图层
|
* @param {Boolean} isContainFixed 是否包含固定图层
|
||||||
|
* @param {Boolean} isContainFixedOther 是否包含其他固定图层
|
||||||
* @returns {Boolean} 是否应该包含
|
* @returns {Boolean} 是否应该包含
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
|
_shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther) {
|
||||||
if (!layer) return false;
|
if (!layer) return false;
|
||||||
|
|
||||||
// 检查背景图层
|
// 检查背景图层
|
||||||
@@ -1047,6 +1062,11 @@ export class ExportManager {
|
|||||||
return isContainFixed;
|
return isContainFixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查其他固定图层
|
||||||
|
if (layer.isFixedOther) {
|
||||||
|
return isContainFixedOther;
|
||||||
|
}
|
||||||
|
|
||||||
// 普通图层总是包含
|
// 普通图层总是包含
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "../commands/ObjectLayerCommands";
|
} from "../commands/ObjectLayerCommands";
|
||||||
import {
|
import {
|
||||||
LayerType,
|
LayerType,
|
||||||
|
SpecialLayerId,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
createLayer,
|
createLayer,
|
||||||
createBackgroundLayer,
|
createBackgroundLayer,
|
||||||
@@ -343,35 +344,36 @@ export class LayerManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 批量更新对象
|
// 批量更新对象
|
||||||
objects.forEach(async (obj) => {
|
for(let obj of objects){
|
||||||
const layer = layerMap[obj.layerId];
|
let layer = layerMap[obj.layerId];
|
||||||
|
|
||||||
if (!obj.layerId) {
|
if (!obj.layerId) {
|
||||||
// 没有关联图层的对象使用默认设置
|
// 没有关联图层的对象使用默认设置
|
||||||
obj.selectable = false;
|
obj.selectable = false;
|
||||||
obj.evented = false;
|
obj.evented = false;
|
||||||
obj.erasable = false; // 未关联图层的对象不可擦除
|
obj.erasable = false; // 未关联图层的对象不可擦除
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!layer) return;
|
if (!layer) break;
|
||||||
|
|
||||||
// 设置一级图层对象的交互性
|
// 设置一级图层对象的交互性
|
||||||
await this._setObjectInteractivity(obj, layer, editorMode);
|
await this._setObjectInteractivity(obj, layer, editorMode);
|
||||||
|
|
||||||
// 设置子图层对象的交互性
|
// 设置子图层对象的交互性
|
||||||
layer?.children?.forEach(async (childLayer) => {
|
for(let childLayer of layer.children){
|
||||||
const childObj = this.canvas
|
let childObj = this.canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.find((o) => o.layerId === childLayer.id);
|
.find((o) => o.layerId === childLayer.id);
|
||||||
if (childObj) {
|
if (childObj) {
|
||||||
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
await this._setObjectInteractivity(childObj, childLayer, editorMode);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
// 设置裁剪对象
|
// 设置裁剪对象
|
||||||
layers.forEach(async (layer) => {
|
for(let layer of layers){
|
||||||
|
if(layer.id === SpecialLayerId.COLOR) break;
|
||||||
let clippingMaskFabricObject = null;
|
let clippingMaskFabricObject = null;
|
||||||
if (layer.clippingMask) {
|
if (layer.clippingMask) {
|
||||||
// 反序列化 clippingMask
|
// 反序列化 clippingMask
|
||||||
@@ -379,7 +381,7 @@ export class LayerManager {
|
|||||||
layer.clippingMask,
|
layer.clippingMask,
|
||||||
this.canvas
|
this.canvas
|
||||||
);
|
);
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
|
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
// 设置绝对定位
|
// 设置绝对定位
|
||||||
@@ -403,7 +405,7 @@ export class LayerManager {
|
|||||||
.find((o) => o.layerId === childLayer.id);
|
.find((o) => o.layerId === childLayer.id);
|
||||||
if (childObj) {
|
if (childObj) {
|
||||||
childObj.clipPath = clippingMaskFabricObject;
|
childObj.clipPath = clippingMaskFabricObject;
|
||||||
childObj.dirty = true; // 标记为脏对象
|
// childObj.dirty = true; // 标记为脏对象
|
||||||
childObj.setCoords();
|
childObj.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +501,7 @@ export class LayerManager {
|
|||||||
isOldSelectObject
|
isOldSelectObject
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -522,15 +524,16 @@ export class LayerManager {
|
|||||||
* @param {string} name 图层名称
|
* @param {string} name 图层名称
|
||||||
* @param {string} type 图层类型
|
* @param {string} type 图层类型
|
||||||
* @param {Object} options 额外选项
|
* @param {Object} options 额外选项
|
||||||
|
* @param {boolean} isCmd 是否创建命令
|
||||||
* @returns {string} 新创建的图层ID
|
* @returns {string} 新创建的图层ID
|
||||||
*/
|
*/
|
||||||
async createLayer(name = null, type = LayerType.EMPTY, options = {}) {
|
async createLayer(name = null, type = LayerType.EMPTY, options = {}, isCmd = true) {
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
const layerId = options.id || options.layerId || generateId("layer_");
|
const layerId = options.id || options.layerId || generateId("layer_");
|
||||||
|
|
||||||
// 计算普通图层数量(非背景、非固定)
|
// 计算普通图层数量(非背景、非固定)
|
||||||
const normalLayersCount = this.layers.value.filter(
|
const normalLayersCount = this.layers.value.filter(
|
||||||
(layer) => !layer.isBackground && !layer.isFixed
|
(layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther
|
||||||
).length;
|
).length;
|
||||||
// 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置
|
// 计算插入位置,如果没有指定insertIndex,则根据当前选中图层决定插入位置
|
||||||
// 添加到图层列表
|
// 添加到图层列表
|
||||||
@@ -542,7 +545,7 @@ export class LayerManager {
|
|||||||
// 创建新图层
|
// 创建新图层
|
||||||
const newLayer = createLayer({
|
const newLayer = createLayer({
|
||||||
id: layerId,
|
id: layerId,
|
||||||
name: name || `图层 ${normalLayersCount + 1}`,
|
name: name || this.t("Canvas.EmptyLayer"),
|
||||||
type: type,
|
type: type,
|
||||||
visible: true,
|
visible: true,
|
||||||
locked: false,
|
locked: false,
|
||||||
@@ -571,13 +574,13 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行命令
|
// 执行命令
|
||||||
if (this.commandManager) {
|
if (isCmd && this.commandManager) {
|
||||||
await this.commandManager.execute(command);
|
await this.commandManager.execute(command);
|
||||||
} else {
|
} else{
|
||||||
await command.execute();
|
await command.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
return layerId;
|
return isCmd ? layerId : command;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -952,18 +955,28 @@ export class LayerManager {
|
|||||||
// 查找要删除的图层
|
// 查找要删除的图层
|
||||||
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
|
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
|
||||||
// 如果是背景层或固定层,不允许删除
|
// 如果是背景层或固定层,不允许删除
|
||||||
if (layer && (layer.isBackground || layer.isFixed)) {
|
if (layer && (layer.isBackground || layer.isFixed || layer.isFixedOther)) {
|
||||||
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
||||||
message.warning(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
|
message.warning(layer.isBackground ? this.t("Canvas.backLayerCannotDelete") : this.t("Canvas.fixedLayerCannotDelete"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 检查是否是唯一的普通图层
|
// 检查是否是唯一的普通图层
|
||||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
var isChild = false;
|
||||||
|
var parentLength = 0;
|
||||||
|
const normalLayers = this.layers.value.filter((layer) => {
|
||||||
|
if(layer.children.some(v => v.id == layerId)){
|
||||||
|
isChild = true;
|
||||||
|
parentLength = layer.children.length;
|
||||||
|
}
|
||||||
|
return !layer.isFixed && !layer.isFixedOther && !layer.isBackground
|
||||||
|
})
|
||||||
|
// const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||||
console.log("普通图层:", normalLayers)
|
console.log("普通图层:", normalLayers)
|
||||||
if (normalLayers.length === 1) {
|
if (isChild ? parentLength <= 1 : false) {//normalLayers.length <= 1
|
||||||
console.warn("不能删除唯一的普通图层");
|
console.warn("不能删除唯一的普通图层");
|
||||||
message.warning("不能删除唯一的普通图层");
|
message.warning(this.t("Canvas.cannotDeleteOnlyLayer"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// // 如果图层有子图层,提示确认
|
// // 如果图层有子图层,提示确认
|
||||||
@@ -1132,7 +1145,7 @@ export class LayerManager {
|
|||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
console.log("==========", allObjects)
|
||||||
// if (layer.fill) {
|
// if (layer.fill) {
|
||||||
// // 如果图层有填充颜色,设置所有对象的填充颜色
|
// // 如果图层有填充颜色,设置所有对象的填充颜色
|
||||||
// const { object } = findObjectById(this.canvas, layer.fill.id);
|
// const { object } = findObjectById(this.canvas, layer.fill.id);
|
||||||
@@ -1578,6 +1591,12 @@ export class LayerManager {
|
|||||||
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
// 如果b是固定图层而a不是固定图层,b应该排在后面(固定图层在普通图层下方)
|
||||||
if (b.isFixed && !a.isFixed) return -1;
|
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;
|
return 0;
|
||||||
});
|
});
|
||||||
@@ -1848,9 +1867,9 @@ export class LayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是唯一的普通图层
|
// 检查是否是唯一的普通图层
|
||||||
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
|
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
|
||||||
console.log("普通图层:", normalLayers)
|
console.log("普通图层:", normalLayers)
|
||||||
if (normalLayers.length === 1) {
|
if (normalLayers.length <= 1) {
|
||||||
console.warn("不能剪切唯一的普通图层");
|
console.warn("不能剪切唯一的普通图层");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -3250,7 +3269,7 @@ export class LayerManager {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_setupGroupMaskMovementSync(activeSelection, layer) {
|
_setupGroupMaskMovementSync(activeSelection, layer) {
|
||||||
if (!activeSelection || !layer || !layer.clippingMask) {
|
if (!activeSelection || !layer || !layer.clippingMask || layer.isFixedClipMask) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3314,7 +3333,6 @@ export class LayerManager {
|
|||||||
// 计算移动距离
|
// 计算移动距离
|
||||||
const deltaX = target.left - initialLeft;
|
const deltaX = target.left - initialLeft;
|
||||||
const deltaY = target.top - initialTop;
|
const deltaY = target.top - initialTop;
|
||||||
|
|
||||||
// 创建更新遮罩位置的命令
|
// 创建更新遮罩位置的命令
|
||||||
const command = new UpdateGroupMaskPositionCommand({
|
const command = new UpdateGroupMaskPositionCommand({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
@@ -3419,4 +3437,22 @@ export class LayerManager {
|
|||||||
|
|
||||||
console.log("🎨 已设置组遮罩移动同步 - 使用 object:modified 事件");
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ export class ThumbnailManager {
|
|||||||
// 重新创建遮罩对象
|
// 重新创建遮罩对象
|
||||||
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
|
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
|
||||||
|
|
||||||
clippingMaskFabricObject.clipPath = null;
|
// clippingMaskFabricObject.clipPath = null;
|
||||||
clippingMaskFabricObject.set({
|
clippingMaskFabricObject.set({
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
clippingMaskFabricObject.dirty = true;
|
// clippingMaskFabricObject.dirty = true;
|
||||||
clippingMaskFabricObject.setCoords();
|
clippingMaskFabricObject.setCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +128,13 @@ export class ThumbnailManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
||||||
let layersToRasterize = [];
|
|
||||||
|
|
||||||
|
if (!layer) {
|
||||||
|
console.warn("⚠️ 无效的图层,无法收集对象");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let layersToRasterize = [];
|
||||||
if (layer.children && layer.children.length > 0) {
|
if (layer.children && layer.children.length > 0) {
|
||||||
// 组图层:收集自身和所有子图层
|
// 组图层:收集自身和所有子图层
|
||||||
layersToRasterize = this._collectLayersToRasterize(layer);
|
layersToRasterize = this._collectLayersToRasterize(layer);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class AnimationManager {
|
|||||||
|
|
||||||
// 如果变化太小,直接应用缩放
|
// 如果变化太小,直接应用缩放
|
||||||
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
||||||
// this._applyZoom(point, targetZoom);
|
this._applyZoom(point, targetZoom);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ export class AnimationManager {
|
|||||||
this._zoomAnimation = null;
|
this._zoomAnimation = null;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ export class AnimationManager {
|
|||||||
this._zoomAnimation = null;
|
this._zoomAnimation = null;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -817,7 +817,7 @@ export class AnimationManager {
|
|||||||
this._wasZooming = false;
|
this._wasZooming = false;
|
||||||
|
|
||||||
// 确保最终状态准确
|
// 确保最终状态准确
|
||||||
// this._applyZoom(point, targetZoom, true);
|
this._applyZoom(point, targetZoom, true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PerformanceManager } from "./PerformanceManager.js";
|
|||||||
*/
|
*/
|
||||||
export class CommandManager {
|
export class CommandManager {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
|
this.canvas = options.canvas;
|
||||||
this.undoStack = [];
|
this.undoStack = [];
|
||||||
this.redoStack = [];
|
this.redoStack = [];
|
||||||
this.maxHistorySize = options.maxHistorySize || 50;
|
this.maxHistorySize = options.maxHistorySize || 50;
|
||||||
@@ -205,6 +206,7 @@ export class CommandManager {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.canvas?.discardActiveObject();
|
||||||
const command = this.undoStack.pop();
|
const command = this.undoStack.pop();
|
||||||
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
|
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
|
||||||
|
|
||||||
@@ -243,6 +245,7 @@ export class CommandManager {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.canvas?.discardActiveObject();
|
||||||
const command = this.redoStack.pop();
|
const command = this.redoStack.pop();
|
||||||
console.log(`↪️ 重做命令: ${command.constructor.name}`);
|
console.log(`↪️ 重做命令: ${command.constructor.name}`);
|
||||||
|
|
||||||
|
|||||||
@@ -688,7 +688,6 @@ export class CanvasEventManager {
|
|||||||
this.layerManager.commandManager.execute(transformCmd, {
|
this.layerManager.commandManager.execute(transformCmd, {
|
||||||
name: "对象修改",
|
name: "对象修改",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清除临时状态记录
|
// 清除临时状态记录
|
||||||
delete activeObj._initialTransformState;
|
delete activeObj._initialTransformState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class KeyboardManager {
|
|||||||
* @param {Object} options.toolManager 工具管理器实例
|
* @param {Object} options.toolManager 工具管理器实例
|
||||||
* @param {Object} options.commandManager 命令管理器实例
|
* @param {Object} options.commandManager 命令管理器实例
|
||||||
* @param {Object} options.layerManager 图层管理器实例
|
* @param {Object} options.layerManager 图层管理器实例
|
||||||
|
* @param {Object} options.canvasManager 画布管理器实例
|
||||||
* @param {Function} options.pasteText 粘贴文本回调函数
|
* @param {Function} options.pasteText 粘贴文本回调函数
|
||||||
* @param {Function} options.pasteImage 粘贴图片回调函数
|
* @param {Function} options.pasteImage 粘贴图片回调函数
|
||||||
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
|
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
|
||||||
@@ -19,6 +20,7 @@ export class KeyboardManager {
|
|||||||
this.toolManager = options.toolManager;
|
this.toolManager = options.toolManager;
|
||||||
this.commandManager = options.commandManager;
|
this.commandManager = options.commandManager;
|
||||||
this.layerManager = options.layerManager;
|
this.layerManager = options.layerManager;
|
||||||
|
this.canvasManager = options.canvasManager;
|
||||||
this.container = options.container || document;
|
this.container = options.container || document;
|
||||||
this.pasteText = options.pasteText || (() => {});
|
this.pasteText = options.pasteText || (() => {});
|
||||||
this.pasteImage = options.pasteImage || (() => {});
|
this.pasteImage = options.pasteImage || (() => {});
|
||||||
@@ -125,6 +127,10 @@ export class KeyboardManager {
|
|||||||
// 删除
|
// 删除
|
||||||
delete: { action: "delete", description: "删除" },
|
delete: { action: "delete", description: "删除" },
|
||||||
backspace: { 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: "全选" },
|
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
|
||||||
@@ -488,6 +494,14 @@ export class KeyboardManager {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "up":
|
||||||
|
case "down":
|
||||||
|
case "left":
|
||||||
|
case "right":
|
||||||
|
// 方向键逻辑
|
||||||
|
this.canvasManager.moveActiveObject(action);
|
||||||
|
break;
|
||||||
|
|
||||||
case "increaseBrushSize":
|
case "increaseBrushSize":
|
||||||
// 增大画笔尺寸
|
// 增大画笔尺寸
|
||||||
if (this.toolManager && this.toolManager.brushManager) {
|
if (this.toolManager && this.toolManager.brushManager) {
|
||||||
@@ -639,7 +653,6 @@ export class KeyboardManager {
|
|||||||
if (event.altKey) shortcutKey += "alt+";
|
if (event.altKey) shortcutKey += "alt+";
|
||||||
|
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
|
|
||||||
// 特殊键处理
|
// 特殊键处理
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case " ":
|
case " ":
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class LiquifyCPUManager {
|
|||||||
sharpenAmount: 0.3, // 添加锐化强度参数
|
sharpenAmount: 0.3, // 添加锐化强度参数
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
console.log("CPU版本的液化管理器config",this.config);
|
console.log("CPU版本的液化管理器config", this.config);
|
||||||
|
|
||||||
this.params = {
|
this.params = {
|
||||||
size: 60, // 增大默认尺寸
|
size: 60, // 增大默认尺寸
|
||||||
@@ -63,7 +63,8 @@ export class LiquifyCPUManager {
|
|||||||
// 新增:持续按压相关状态
|
// 新增:持续按压相关状态
|
||||||
this.pressStartTime = 0; // 按压开始时间
|
this.pressStartTime = 0; // 按压开始时间
|
||||||
this.pressDuration = 0; // 按压持续时间
|
this.pressDuration = 0; // 按压持续时间
|
||||||
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)
|
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)--废除使用固定角度
|
||||||
|
this.fixedRotationAngle = 0.32; // 固定旋转角度
|
||||||
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
|
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
|
||||||
this.lastApplyTime = 0; // 上次应用时间
|
this.lastApplyTime = 0; // 上次应用时间
|
||||||
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
|
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
|
||||||
@@ -189,7 +190,7 @@ export class LiquifyCPUManager {
|
|||||||
this.isHolding = true;
|
this.isHolding = true;
|
||||||
|
|
||||||
// 启动持续效果定时器(对于所有模式都支持持续按压)
|
// 启动持续效果定时器(对于所有模式都支持持续按压)
|
||||||
this.startContinuousEffect();
|
// this.startContinuousEffect();
|
||||||
|
|
||||||
console.log(`开始液化操作,初始点: (${x}, ${y})`);
|
console.log(`开始液化操作,初始点: (${x}, ${y})`);
|
||||||
}
|
}
|
||||||
@@ -220,7 +221,6 @@ export class LiquifyCPUManager {
|
|||||||
// 新增:启动持续效果
|
// 新增:启动持续效果
|
||||||
startContinuousEffect() {
|
startContinuousEffect() {
|
||||||
this.stopContinuousEffect(); // 先停止已有的定时器
|
this.stopContinuousEffect(); // 先停止已有的定时器
|
||||||
|
|
||||||
this.continuousTimer = setInterval(() => {
|
this.continuousTimer = setInterval(() => {
|
||||||
if (this.isHolding && this.initialized) {
|
if (this.isHolding && this.initialized) {
|
||||||
// 更新持续时间
|
// 更新持续时间
|
||||||
@@ -273,7 +273,6 @@ export class LiquifyCPUManager {
|
|||||||
*/
|
*/
|
||||||
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
|
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
|
||||||
if (!this.currentImageData) return;
|
if (!this.currentImageData) return;
|
||||||
|
|
||||||
const data = this.currentImageData.data;
|
const data = this.currentImageData.data;
|
||||||
const width = this.currentImageData.width;
|
const width = this.currentImageData.width;
|
||||||
const height = this.currentImageData.height;
|
const height = this.currentImageData.height;
|
||||||
@@ -286,6 +285,7 @@ export class LiquifyCPUManager {
|
|||||||
const rotationAngle =
|
const rotationAngle =
|
||||||
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
|
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
|
||||||
|
|
||||||
|
console.log("持续应用旋转效果");
|
||||||
// 累积旋转角度 - 关键:这确保了持续旋转效果
|
// 累积旋转角度 - 关键:这确保了持续旋转效果
|
||||||
this.accumulatedRotation += rotationAngle;
|
this.accumulatedRotation += rotationAngle;
|
||||||
|
|
||||||
@@ -309,13 +309,14 @@ export class LiquifyCPUManager {
|
|||||||
|
|
||||||
// 计算旋转后的源位置 - 关键算法
|
// 计算旋转后的源位置 - 关键算法
|
||||||
const angle = Math.atan2(dy, dx);
|
const angle = Math.atan2(dy, dx);
|
||||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
// const newAngle = angle + this.accumulatedRotation * falloff;
|
||||||
|
const newAngle = angle + (isClockwise ? this.fixedRotationAngle : -this.fixedRotationAngle) * falloff;
|
||||||
|
|
||||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||||
|
|
||||||
// 双线性插值采样 - 确保像素连续性
|
// 双线性插值采样 - 确保像素连续性
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -376,7 +377,7 @@ export class LiquifyCPUManager {
|
|||||||
const sourceY = centerY + dy * scale;
|
const sourceY = centerY + dy * scale;
|
||||||
|
|
||||||
// 双线性插值采样
|
// 双线性插值采样
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -401,16 +402,17 @@ export class LiquifyCPUManager {
|
|||||||
*/
|
*/
|
||||||
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
|
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
|
||||||
if (!this.currentImageData) return;
|
if (!this.currentImageData) return;
|
||||||
|
|
||||||
const data = this.currentImageData.data;
|
const data = this.currentImageData.data;
|
||||||
const width = this.currentImageData.width;
|
const width = this.currentImageData.width;
|
||||||
const height = this.currentImageData.height;
|
const height = this.currentImageData.height;
|
||||||
const tempData = new Uint8ClampedArray(data);
|
const tempData = new Uint8ClampedArray(data);
|
||||||
|
|
||||||
// 计算推拉方向
|
// 计算推拉方向
|
||||||
const deltaX = this.currentMouseX - this.initialMouseX;
|
const deltaX = this.currentMouseX - this.lastMouseX;
|
||||||
const deltaY = this.currentMouseY - this.initialMouseY;
|
const deltaY = this.currentMouseY - this.lastMouseY;
|
||||||
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
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 processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||||
@@ -426,6 +428,7 @@ export class LiquifyCPUManager {
|
|||||||
|
|
||||||
for (let y = minY; y < maxY; y++) {
|
for (let y = minY; y < maxY; y++) {
|
||||||
for (let x = minX; x < maxX; x++) {
|
for (let x = minX; x < maxX; x++) {
|
||||||
|
// 此处循环4万次
|
||||||
const dx = x - centerX;
|
const dx = x - centerX;
|
||||||
const dy = y - centerY;
|
const dy = y - centerY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@@ -442,7 +445,7 @@ export class LiquifyCPUManager {
|
|||||||
const sourceX = x - pushX;
|
const sourceX = x - pushX;
|
||||||
const sourceY = y - pushY;
|
const sourceY = y - pushY;
|
||||||
|
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -461,9 +464,9 @@ export class LiquifyCPUManager {
|
|||||||
// 有拖拽时的推拉效果
|
// 有拖拽时的推拉效果
|
||||||
const dirX = deltaX / dragLength;
|
const dirX = deltaX / dragLength;
|
||||||
const dirY = deltaY / dragLength;
|
const dirY = deltaY / dragLength;
|
||||||
|
|
||||||
for (let y = minY; y < maxY; y++) {
|
for (let y = minY; y < maxY; y++) {
|
||||||
for (let x = minX; x < maxX; x++) {
|
for (let x = minX; x < maxX; x++) {
|
||||||
|
// 此处循环4万次
|
||||||
const dx = x - centerX;
|
const dx = x - centerX;
|
||||||
const dy = y - centerY;
|
const dy = y - centerY;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
@@ -473,13 +476,13 @@ export class LiquifyCPUManager {
|
|||||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||||
const factor = falloff * strength;
|
const factor = falloff * strength;
|
||||||
|
|
||||||
const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15);
|
const offsetX = dirX * factor * Math.min(dragLength * 2, 30);
|
||||||
const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15);
|
const offsetY = dirY * factor * Math.min(dragLength * 2, 30);
|
||||||
|
|
||||||
const sourceX = x - offsetX;
|
const sourceX = x - offsetX;
|
||||||
const sourceY = y - offsetY;
|
const sourceY = y - offsetY;
|
||||||
|
|
||||||
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
|
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
const targetIdx = (y * width + x) * 4;
|
const targetIdx = (y * width + x) * 4;
|
||||||
@@ -527,7 +530,7 @@ export class LiquifyCPUManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case this.modes.PUSH:
|
case this.modes.PUSH:
|
||||||
this._applyEnhancedPushDeformation(x, y, radius, strength);
|
// this._applyEnhancedPushDeformation(x, y, radius, strength);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
@@ -553,101 +556,7 @@ export class LiquifyCPUManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用液化变形 - 主要入口,集成增强算法
|
* 双线性插值函数
|
||||||
*/
|
|
||||||
// applyDeformation(x, y) {
|
|
||||||
// if (!this.initialized || !this.originalImageData) {
|
|
||||||
// console.warn("液化管理器未初始化或缺少必要数据");
|
|
||||||
// return this.currentImageData;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 更新鼠标位置
|
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双线性插值采样 - 用于像素级算法
|
|
||||||
* @param {Uint8ClampedArray} data 图像数据
|
* @param {Uint8ClampedArray} data 图像数据
|
||||||
* @param {number} width 图像宽度
|
* @param {number} width 图像宽度
|
||||||
* @param {number} height 图像高度
|
* @param {number} height 图像高度
|
||||||
@@ -655,19 +564,55 @@ export class LiquifyCPUManager {
|
|||||||
* @param {number} y Y坐标
|
* @param {number} y Y坐标
|
||||||
* @returns {Array|null} RGBA颜色值数组或null
|
* @returns {Array|null} RGBA颜色值数组或null
|
||||||
*/
|
*/
|
||||||
_bilinearSample(data, width, height, x, y) {
|
_bilinearInterpolate(data, width, height, x, y) {
|
||||||
return this._bicubicInterpolate(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);
|
||||||
|
|
||||||
|
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)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 双三次插值实现 - 确保正确处理Alpha通道
|
* 三次插值实现 - 确保正确处理Alpha通道
|
||||||
* @param {Uint8ClampedArray} data 图像数据
|
* @param {Uint8ClampedArray} data 图像数据
|
||||||
* @param {number} width 图像宽度
|
* @param {number} width 图像宽度
|
||||||
* @param {number} height 图像高度
|
* @param {number} height 图像高度
|
||||||
* @param {number} x X坐标
|
* @param {number} x X坐标
|
||||||
* @param {number} y Y坐标
|
* @param {number} y Y坐标
|
||||||
* @returns {Array|null} RGBA颜色值数组或null
|
* @returns {Array|null} RGBA颜色值数组或null
|
||||||
*/
|
*/
|
||||||
_bicubicInterpolate(data, width, height, x, y) {
|
_bicubicInterpolate(data, width, height, x, y) {
|
||||||
|
// return this._bilinearInterpolate(data, width, height, x, y);
|
||||||
|
|
||||||
// 获取周围16个像素点
|
// 获取周围16个像素点
|
||||||
const x1 = Math.floor(x) - 1;
|
const x1 = Math.floor(x) - 1;
|
||||||
const y1 = Math.floor(y) - 1;
|
const y1 = Math.floor(y) - 1;
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export class LiquifyWebGLManager {
|
|||||||
this.isHolding = true;
|
this.isHolding = true;
|
||||||
|
|
||||||
// 启动持续效果定时器
|
// 启动持续效果定时器
|
||||||
this.startContinuousEffect();
|
// this.startContinuousEffect();
|
||||||
|
|
||||||
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export class LayerSort {
|
|||||||
} else if (layer.isFixed && layer.fabricObject) {
|
} else if (layer.isFixed && layer.fabricObject) {
|
||||||
// 固定图层对象
|
// 固定图层对象
|
||||||
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
|
} else if (layer.isFixedOther && layer.fabricObject) {
|
||||||
|
// 其他固定图层对象
|
||||||
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
} else if (!layer.isBackground && !layer.isFixed) {
|
} else if (!layer.isBackground && !layer.isFixed) {
|
||||||
// 普通图层
|
// 普通图层
|
||||||
currentZIndex = this.processLayerObjects(
|
currentZIndex = this.processLayerObjects(
|
||||||
|
|||||||
37
src/component/Canvas/CanvasEditor/utils/event.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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();
|
||||||
@@ -429,7 +429,8 @@ export function objectIsInCanvas(canvas, targetObj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetId = targetObj.id;
|
const targetId = targetObj.id;
|
||||||
if (!targetId) {
|
const targetLayerId = targetObj.layerId;
|
||||||
|
if (!targetId && !targetLayerId) {
|
||||||
return { flag: false, object: null, parent: null };
|
return { flag: false, object: null, parent: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +438,11 @@ export function objectIsInCanvas(canvas, targetObj) {
|
|||||||
const topLevelObjects = canvas.getObjects();
|
const topLevelObjects = canvas.getObjects();
|
||||||
|
|
||||||
// 直接在顶层查找
|
// 直接在顶层查找
|
||||||
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
|
const directMatch = topLevelObjects.find((obj) => {
|
||||||
|
const isId = !targetId ? true : obj.id === targetId;
|
||||||
|
const isLayerId = !targetLayerId ? true : obj.layerId === targetLayerId;
|
||||||
|
return isId && isLayerId;
|
||||||
|
});
|
||||||
if (directMatch) {
|
if (directMatch) {
|
||||||
return { flag: true, object: directMatch, parent: null };
|
return { flag: true, object: directMatch, parent: null };
|
||||||
}
|
}
|
||||||
@@ -500,6 +505,22 @@ export function findObjectById(canvas, objectId) {
|
|||||||
return { object: result.object, parent: result.parent };
|
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 画布实例
|
* @param {fabric.Canvas} canvas 画布实例
|
||||||
@@ -738,3 +759,252 @@ export function getLayerObjectsZIndex(canvas, layerId) {
|
|||||||
const allInfo = getAllObjectsZIndex(canvas);
|
const allInfo = getAllObjectsZIndex(canvas);
|
||||||
return allInfo.filter((info) => info.layerId === layerId);
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
function initAligningGuidelines(canvas) {
|
function initAligningGuidelines(canvas) {
|
||||||
var ctx = canvas.getSelectionContext(),
|
var ctx = canvas.getSelectionContext(),
|
||||||
aligningLineOffset = 5,
|
aligningLineOffset = 1,
|
||||||
aligningLineMargin = 4,
|
aligningLineMargin = 1,
|
||||||
aligningLineWidth = 1,
|
aligningLineWidth = 1,
|
||||||
aligningLineColor = "rgb(0,255,0)",
|
aligningLineColor = "rgb(0,255,0)",
|
||||||
viewportTransform,
|
viewportTransform,
|
||||||
@@ -14,9 +14,9 @@ function initAligningGuidelines(canvas) {
|
|||||||
|
|
||||||
function drawVerticalLine(coords) {
|
function drawVerticalLine(coords) {
|
||||||
drawLine(
|
drawLine(
|
||||||
coords.x + 0.5,
|
coords.x,
|
||||||
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
|
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
|
||||||
coords.x + 0.5,
|
coords.x,
|
||||||
coords.y2 > coords.y1 ? coords.y2 : coords.y1
|
coords.y2 > coords.y1 ? coords.y2 : coords.y1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,9 @@ function initAligningGuidelines(canvas) {
|
|||||||
function drawHorizontalLine(coords) {
|
function drawHorizontalLine(coords) {
|
||||||
drawLine(
|
drawLine(
|
||||||
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
|
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
|
||||||
coords.y + 0.5,
|
coords.y,
|
||||||
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
|
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
|
||||||
coords.y + 0.5
|
coords.y
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ export function initCenteringGuidelines(canvas) {
|
|||||||
canvasHeightCenter = canvasHeight / 2,
|
canvasHeightCenter = canvasHeight / 2,
|
||||||
canvasWidthCenterMap = {},
|
canvasWidthCenterMap = {},
|
||||||
canvasHeightCenterMap = {},
|
canvasHeightCenterMap = {},
|
||||||
centerLineMargin = 4,
|
centerLineMargin = 1,
|
||||||
centerLineColor = "rgba(255,0,241,0.5)",
|
centerLineColor = "rgba(255,0,241,0.5)",
|
||||||
centerLineWidth = 1,
|
centerLineWidth = 1,
|
||||||
ctx = canvas.getSelectionContext(),
|
ctx = canvas.getSelectionContext(),
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export const LayerType = {
|
|||||||
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊图层ID
|
||||||
|
*/
|
||||||
|
export const SpecialLayerId = {
|
||||||
|
SPECIAL_GROUP: "group_special", // 特殊组
|
||||||
|
COLOR: "special_color", // 颜色图层
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽)....
|
* 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽)....
|
||||||
*/
|
*/
|
||||||
@@ -178,12 +188,17 @@ export function createLayer(options = {}) {
|
|||||||
locked: options.locked !== undefined ? options.locked : false,
|
locked: options.locked !== undefined ? options.locked : false,
|
||||||
opacity: options.opacity !== undefined ? options.opacity : 1.0,
|
opacity: options.opacity !== undefined ? options.opacity : 1.0,
|
||||||
blendMode: options.blendMode || BlendMode.NORMAL,
|
blendMode: options.blendMode || BlendMode.NORMAL,
|
||||||
|
isHidenDragHandle: options.isHidenDragHandle || false,
|
||||||
|
isDisableUnlock: options.isDisableUnlock || false,
|
||||||
|
isFixedOther: options.isFixedOther || false,
|
||||||
|
isFixedClipMask: options.isFixedClipMask || false,
|
||||||
|
|
||||||
// 确保不是背景图层
|
// 确保不是背景图层
|
||||||
isBackground: false,
|
isBackground: false,
|
||||||
|
|
||||||
// Fabric.js 对象列表
|
// Fabric.js 对象列表
|
||||||
fabricObjects: options.fabricObjects || [],
|
fabricObjects: options.fabricObjects || [],
|
||||||
|
fabricObject: options.fabricObject || null,
|
||||||
|
|
||||||
// 嵌套结构 - 适用于组图层
|
// 嵌套结构 - 适用于组图层
|
||||||
children: options.children || [],
|
children: options.children || [],
|
||||||
|
|||||||
@@ -155,15 +155,19 @@ export function validateLayerAssociations(layers, canvasObjects) {
|
|||||||
/**
|
/**
|
||||||
* 简化layers对象属性,只保留必要的属性
|
* 简化layers对象属性,只保留必要的属性
|
||||||
* @param {Array} layers 图层数组
|
* @param {Array} layers 图层数组
|
||||||
|
* @param {Array} excludedLayers 排除的图层ID数组
|
||||||
* @returns {Array} 简化后的图层数组
|
* @returns {Array} 简化后的图层数组
|
||||||
*/
|
*/
|
||||||
export function simplifyLayers(layers) {
|
export function simplifyLayers(layers, excludedLayers = []) {
|
||||||
if (!layers || !isArray(layers)) {
|
if (!layers || !isArray(layers)) {
|
||||||
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return layers.map((layer) => {
|
return layers.map((layer) => {
|
||||||
|
// 检查是否在排除列表中
|
||||||
|
if (excludedLayers && excludedLayers.includes(layer.id)) return null;
|
||||||
|
|
||||||
const simplifiedLayer = {
|
const simplifiedLayer = {
|
||||||
id: layer.id,
|
id: layer.id,
|
||||||
name: layer.name,
|
name: layer.name,
|
||||||
@@ -172,6 +176,10 @@ export function simplifyLayers(layers) {
|
|||||||
opacity: layer.opacity,
|
opacity: layer.opacity,
|
||||||
isBackground: layer.isBackground || false,
|
isBackground: layer.isBackground || false,
|
||||||
isFixed: layer.isFixed || false,
|
isFixed: layer.isFixed || false,
|
||||||
|
isFixedOther: layer.isFixedOther || false,
|
||||||
|
isFixedClipMask: layer.isFixedClipMask || false,
|
||||||
|
isHidenDragHandle: layer.isHidenDragHandle || false,
|
||||||
|
isDisableUnlock: layer.isDisableUnlock || false,
|
||||||
clippingMask:
|
clippingMask:
|
||||||
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
|
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
|
||||||
layer.clippingMask ||
|
layer.clippingMask ||
|
||||||
@@ -207,10 +215,11 @@ export function simplifyLayers(layers) {
|
|||||||
fill: layer?.fill || null,
|
fill: layer?.fill || null,
|
||||||
fillColor: layer.fillColor,
|
fillColor: layer.fillColor,
|
||||||
selectObject: layer.selectObject,
|
selectObject: layer.selectObject,
|
||||||
|
blendMode: layer.blendMode || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return simplifiedLayer;
|
return simplifiedLayer;
|
||||||
});
|
}).filter((layer) => !!layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,55 +7,106 @@ import { fabric } from "fabric-with-all";
|
|||||||
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
|
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
|
||||||
*/
|
*/
|
||||||
export async function restoreFabricObject(serializedObject, canvas) {
|
export async function restoreFabricObject(serializedObject, canvas) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const objectType = serializedObject.type;
|
const objectType = serializedObject.type;
|
||||||
// 定义恢复后的处理函数
|
// 定义恢复后的处理函数
|
||||||
const handleRestoredObject = (fabricObject) => {
|
const handleRestoredObject = (fabricObject) => {
|
||||||
if (!fabricObject) {
|
if (!fabricObject) {
|
||||||
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
|
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 恢复自定义属性
|
||||||
|
if (serializedObject.id) fabricObject.id = serializedObject.id;
|
||||||
|
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
|
||||||
|
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
|
||||||
|
|
||||||
// 恢复自定义属性
|
// 更新坐标
|
||||||
if (serializedObject.id) fabricObject.id = serializedObject.id;
|
fabricObject.setCoords();
|
||||||
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
|
|
||||||
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
|
|
||||||
|
|
||||||
// 更新坐标
|
// 添加到画布
|
||||||
fabricObject.setCoords();
|
// canvas.add(fabricObject);
|
||||||
|
|
||||||
// 添加到画布
|
resolve(fabricObject);
|
||||||
// canvas.add(fabricObject);
|
};
|
||||||
|
|
||||||
resolve(fabricObject);
|
// 根据类型选择恢复方法
|
||||||
};
|
switch (objectType) {
|
||||||
|
case "rect":
|
||||||
// 根据类型选择恢复方法
|
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
|
||||||
switch (objectType) {
|
break;
|
||||||
case "rect":
|
case "circle":
|
||||||
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
|
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "circle":
|
case "path":
|
||||||
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
|
fabric.Path.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "path":
|
case "image":
|
||||||
fabric.Path.fromObject(serializedObject, handleRestoredObject);
|
fabric.Image.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "image":
|
case "group":
|
||||||
fabric.Image.fromObject(serializedObject, handleRestoredObject);
|
fabric.Group.fromObject(serializedObject, handleRestoredObject);
|
||||||
break;
|
break;
|
||||||
case "group":
|
default:
|
||||||
fabric.Group.fromObject(serializedObject, handleRestoredObject);
|
// 使用通用方法
|
||||||
break;
|
fabric.util.enlivenObjects([serializedObject], (objects) => {
|
||||||
default:
|
if (objects && objects[0]) {
|
||||||
// 使用通用方法
|
handleRestoredObject(objects[0]);
|
||||||
fabric.util.enlivenObjects([serializedObject], (objects) => {
|
} else {
|
||||||
if (objects && objects[0]) {
|
reject(new Error("对象恢复失败"));
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// 栅格化帮助
|
// 栅格化帮助
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
|
import { SpecialLayerId } from "./layerHelper";
|
||||||
/**
|
/**
|
||||||
* 创建栅格化图像 - 重构版本
|
* 创建栅格化图像 - 重构版本
|
||||||
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
|
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
|
||||||
@@ -68,7 +68,7 @@ export const createRasterizedImage = async ({
|
|||||||
isReturenDataURL,
|
isReturenDataURL,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("创建栅格化图像失败:", error);
|
console.warn("创建栅格化图像失败:", error);
|
||||||
throw new Error(`栅格化失败: ${error.message}`);
|
throw new Error(`栅格化失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ const createClippedObjects = async ({
|
|||||||
console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
||||||
return fabricImage;
|
return fabricImage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("创建裁剪对象失败:", error);
|
console.warn("创建裁剪对象失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -184,10 +184,16 @@ const createClippedDataURLByCanvas = async ({
|
|||||||
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
|
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
|
||||||
|
|
||||||
// 使用优化后的边界计算,确保包含描边区域
|
// 使用优化后的边界计算,确保包含描边区域
|
||||||
const optimizedBounds = calculateOptimizedBounds(
|
// const optimizedBounds = calculateOptimizedBounds(
|
||||||
clippingObject,
|
// clippingObject,
|
||||||
fabricObjects
|
// fabricObjects
|
||||||
);
|
// );
|
||||||
|
const optimizedBounds = {
|
||||||
|
left: clippingObject.left - clippingObject.width / 2,
|
||||||
|
top: clippingObject.top - clippingObject.height / 2,
|
||||||
|
width: clippingObject.width,
|
||||||
|
height: clippingObject.height,
|
||||||
|
}
|
||||||
|
|
||||||
// 使用高分辨率以保证质量
|
// 使用高分辨率以保证质量
|
||||||
const pixelRatio = window.devicePixelRatio || 1;
|
const pixelRatio = window.devicePixelRatio || 1;
|
||||||
@@ -685,6 +691,16 @@ const cloneObjectAsync = (obj) => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
obj.clone((cloned) => {
|
obj.clone((cloned) => {
|
||||||
if (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);
|
resolve(cloned);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("对象克隆失败"));
|
reject(new Error("对象克隆失败"));
|
||||||
@@ -839,9 +855,8 @@ const renderContentToImage = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 克隆并添加所有需要渲染的对象
|
// 克隆并添加所有需要渲染的对象
|
||||||
for (const obj of fabricObjects) {
|
for (let obj of fabricObjects) {
|
||||||
const clonedObj = await cloneObjectAsync(obj);
|
let clonedObj = await cloneObjectAsync(obj);
|
||||||
|
|
||||||
// 调整对象位置:将选区左上角作为新的原点(0,0)
|
// 调整对象位置:将选区左上角作为新的原点(0,0)
|
||||||
clonedObj.set({
|
clonedObj.set({
|
||||||
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
|
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
|
||||||
@@ -853,19 +868,19 @@ const renderContentToImage = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 如果有裁剪路径,也需要调整裁剪路径
|
// 如果有裁剪路径,也需要调整裁剪路径
|
||||||
if (clonedObj.clipPath) {
|
if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) {
|
||||||
clonedObj.clipPath.set({
|
clonedObj.clipPath.set({
|
||||||
left:
|
left: (clonedObj.clipPath.left - selectionBounds.left) * qualityMultiplier,
|
||||||
(clonedObj.clipPath.left - selectionBounds.left) *
|
top: (clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier,
|
||||||
qualityMultiplier,
|
|
||||||
top:
|
|
||||||
(clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier,
|
|
||||||
scaleX: (clonedObj.clipPath.scaleX || 1) * qualityMultiplier,
|
scaleX: (clonedObj.clipPath.scaleX || 1) * qualityMultiplier,
|
||||||
scaleY: (clonedObj.clipPath.scaleY || 1) * qualityMultiplier,
|
scaleY: (clonedObj.clipPath.scaleY || 1) * qualityMultiplier,
|
||||||
});
|
});
|
||||||
clonedObj.clipPath.setCoords(); // 更新裁剪路径坐标
|
clonedObj.clipPath.setCoords(); // 更新裁剪路径坐标
|
||||||
}
|
}
|
||||||
|
// if(obj.globalCompositeOperation === "multiply"){
|
||||||
|
// clonedObj.clipPath = null;
|
||||||
|
// }
|
||||||
|
console.log("==========", obj.id, obj.layerName);
|
||||||
contentCanvas.add(clonedObj);
|
contentCanvas.add(clonedObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1239,7 +1254,7 @@ const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
|
|||||||
|
|
||||||
return optimizedBounds;
|
return optimizedBounds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("计算优化边界框失败:", error);
|
console.warn("计算优化边界框失败:", error);
|
||||||
// 返回原始计算方式作为备选
|
// 返回原始计算方式作为备选
|
||||||
return clippingObject.getBoundingRect(true, true);
|
return clippingObject.getBoundingRect(true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ onUnmounted(() => {
|
|||||||
:style="tool.style"
|
:style="tool.style"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
<SvgIcon v-if="tool.icon" :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
||||||
|
<span v-else>{{ tool.label }}</span>
|
||||||
<teleport to="body" v-if="tipBody">
|
<teleport to="body" v-if="tipBody">
|
||||||
<div class="tool-tooltip" :id="tipId">{{ t(tool.title) }}</div>
|
<div class="tool-tooltip" :id="tipId">{{ t(tool.title) }}</div>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const changeFixedImage = () => {
|
|||||||
canvasEditor.value.changeFixedImage(changeImageUrl);
|
canvasEditor.value.changeFixedImage(changeImageUrl);
|
||||||
};
|
};
|
||||||
const frontBackChange = (value) =>{
|
const frontBackChange = (value) =>{
|
||||||
console.log(value)
|
console.log("==========红绿图导出url", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件挂载时绑定键盘事件
|
// 组件挂载时绑定键盘事件
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import ToolButton from "@/component/Canvas/ExistsImageList/ToolButton.vue";
|
|||||||
const canvasEditor = ref();
|
const canvasEditor = ref();
|
||||||
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
|
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
|
||||||
|
|
||||||
const clothingImageUrl = "/src/assets/work/3.PNG";
|
const clothingImageUrl = "/src/assets/images/canvas/xiangao.png";
|
||||||
const clothingImageUrlInit = "/src/assets/work/5.PNG";
|
const clothingImageUrlInit = "/src/assets/images/canvas/xiangaofenge.png";
|
||||||
|
|
||||||
const imageData = [
|
const imageData = [
|
||||||
{
|
{
|
||||||
@@ -71,8 +71,10 @@ const editorConfig = {
|
|||||||
const exportImage = async () => {
|
const exportImage = async () => {
|
||||||
if (canvasEditor.value) {
|
if (canvasEditor.value) {
|
||||||
const base64 = await canvasEditor.value.exportImage({
|
const base64 = await canvasEditor.value.exportImage({
|
||||||
isContainFixed: true, // 是否导出底图
|
isContainFixed: false, // 是否导出底图
|
||||||
|
isContainFixedOther: false, // 是否导出其他固定图层
|
||||||
isContainBg: false, // 是否导出背景
|
isContainBg: false, // 是否导出背景
|
||||||
|
isEnhanceImg: false, // 是否导出增强图片
|
||||||
});
|
});
|
||||||
|
|
||||||
// 模拟下载图片
|
// 模拟下载图片
|
||||||
@@ -84,6 +86,30 @@ const exportImage = async () => {
|
|||||||
document.body.removeChild(link); // 下载后移除链接元素
|
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) => {
|
const changeCanvas = (command) => {
|
||||||
console.log(command);
|
console.log(command);
|
||||||
@@ -106,32 +132,6 @@ 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() {
|
function exportAsPNG() {
|
||||||
console.log("导出PNG");
|
console.log("导出PNG");
|
||||||
@@ -201,10 +201,130 @@ const canvasInit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const frontBackChange =(value)=>{
|
const frontBackChange =(value)=>{
|
||||||
console.log(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isShowLeft = ref(true);
|
// 自定义工具配置相关
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -232,8 +352,10 @@ const isShowLeft = ref(true);
|
|||||||
ref="canvasEditor"
|
ref="canvasEditor"
|
||||||
key="canvasEditor"
|
key="canvasEditor"
|
||||||
v-if="currentView === 'canvasEditor'"
|
v-if="currentView === 'canvasEditor'"
|
||||||
|
:clothingImageUrl="clothingImageUrl"
|
||||||
|
:clothingImageUrl2="clothingImageUrlInit"
|
||||||
|
:otherData="otherData"
|
||||||
:config="editorConfig"
|
:config="editorConfig"
|
||||||
:clothingImageUrl="clothingImageUrl"
|
|
||||||
:clothing-image-opts="{
|
:clothing-image-opts="{
|
||||||
imageMode: 'contains', // 设置底图包含在画布内
|
imageMode: 'contains', // 设置底图包含在画布内
|
||||||
}"
|
}"
|
||||||
@@ -250,46 +372,15 @@ const isShowLeft = ref(true);
|
|||||||
<template #customToolsBottom="{ toolButtonProps }">
|
<template #customToolsBottom="{ toolButtonProps }">
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<div class="tool-separator"></div>
|
<div class="tool-separator"></div>
|
||||||
|
<!-- 自定义工具按钮 -->
|
||||||
<!-- 自定义工具按钮 -->
|
|
||||||
<ToolButton
|
<ToolButton
|
||||||
v-for="tool in customToolsList"
|
v-for="tool in customToolsList"
|
||||||
:key="tool.id"
|
:key="tool.id"
|
||||||
:tool="tool"
|
:tool="tool"
|
||||||
:active-tool="toolButtonProps.activeTool"
|
:active-tool="toolButtonProps.activeTool"
|
||||||
@click="handleCustomToolClick"
|
@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>
|
</template>
|
||||||
</CanvasEditor>
|
</CanvasEditor>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,15 +515,17 @@ body {
|
|||||||
|
|
||||||
.tool-tooltip {
|
.tool-tooltip {
|
||||||
display: none;
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
writing-mode: vertical-rl; /* 竖直排列 */
|
||||||
top: 50%;
|
text-orientation: upright; /* 保持文字正常显示 */// left: 100%;
|
||||||
transform: translateY(-50%);
|
left: 50%;
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
top: -0.8rem;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.8rem 0.4rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
margin-left: 0.8rem;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -441,12 +534,12 @@ body {
|
|||||||
.tool-tooltip:before {
|
.tool-tooltip:before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
left: 50%;
|
||||||
right: 100%;
|
bottom: 0;
|
||||||
margin-top: -0.5rem;
|
transform: translate(-50%, 100%);
|
||||||
border-width: 0.5rem;
|
border-width: 0.5rem;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
|
border-color: rgba(0, 0, 0, 0.9) transparent transparent transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色模式适配 */
|
/* 深色模式适配 */
|
||||||
|
|||||||
156
src/component/Canvas/pingpu.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pingpu" ref="el"><canvas ref="canvasRef"></canvas></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { fabric } from "fabric-with-all";
|
||||||
|
import { ref, watch, onMounted } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
url: { type: String, required: true },
|
||||||
|
offsetX: { type: Number, default: 0 }, // px
|
||||||
|
offsetY: { type: Number, default: 0 }, // px
|
||||||
|
angle: { type: Number, default: 0 }, // 角度
|
||||||
|
scale: { type: Number, default: 100 }, // %
|
||||||
|
gapX: { type: Number, default: 0 }, // px
|
||||||
|
gapY: { type: Number, default: 0 }, // px
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.url,
|
||||||
|
() => getOriginalImage()
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
props.offsetX,
|
||||||
|
props.offsetY,
|
||||||
|
props.angle,
|
||||||
|
props.scale,
|
||||||
|
props.gapX,
|
||||||
|
props.gapY,
|
||||||
|
],
|
||||||
|
() => setCanvasData()
|
||||||
|
);
|
||||||
|
const el = ref(null);
|
||||||
|
const canvasRef = ref(null);
|
||||||
|
const canvas = ref(null);
|
||||||
|
const observer = ref(null);
|
||||||
|
const id = "asfs123121sfe";
|
||||||
|
onMounted(async () => {
|
||||||
|
initCanvas();
|
||||||
|
await getOriginalImage();
|
||||||
|
setCanvasData();
|
||||||
|
let throttleTimeout = null;
|
||||||
|
let lastRunTime = 0;
|
||||||
|
let trailingTimeout = null;
|
||||||
|
observer.value = new ResizeObserver((entries) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const throttleDelay = 100;
|
||||||
|
if (!throttleTimeout) {
|
||||||
|
updateCanvasSize();
|
||||||
|
lastRunTime = now;
|
||||||
|
throttleTimeout = setTimeout(() => {
|
||||||
|
throttleTimeout = null;
|
||||||
|
}, throttleDelay);
|
||||||
|
} else {
|
||||||
|
clearTimeout(trailingTimeout);
|
||||||
|
trailingTimeout = setTimeout(() => {
|
||||||
|
updateCanvasSize();
|
||||||
|
lastRunTime = Date.now();
|
||||||
|
}, throttleDelay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.value.observe(el.value);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer.value.disconnect();
|
||||||
|
});
|
||||||
|
const initCanvas = () => {
|
||||||
|
canvas.value = new fabric.Canvas(canvasRef.value, {
|
||||||
|
selection: false,
|
||||||
|
evented: false,
|
||||||
|
});
|
||||||
|
canvas.value.setWidth(el.value.offsetWidth);
|
||||||
|
canvas.value.setHeight(el.value.offsetHeight);
|
||||||
|
};
|
||||||
|
const updateCanvasSize = () => {
|
||||||
|
canvas.value.setWidth(el.value.offsetWidth);
|
||||||
|
canvas.value.setHeight(el.value.offsetHeight);
|
||||||
|
setCanvasData();
|
||||||
|
};
|
||||||
|
const originalImage = ref(null);
|
||||||
|
const getOriginalImage = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fabric.Image.fromURL(
|
||||||
|
props.url,
|
||||||
|
(object) => {
|
||||||
|
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);
|
||||||
|
originalImage.value = tcanvas;
|
||||||
|
resolve(tcanvas);
|
||||||
|
},
|
||||||
|
{ crossOrigin: "anonymous" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const setCanvasData = () => {
|
||||||
|
canvas.value.getObjects().forEach((obj) => {
|
||||||
|
if (obj.id === id) canvas.value.remove(obj);
|
||||||
|
});
|
||||||
|
const image = originalImage.value;
|
||||||
|
const cwidth = canvas.value.width;
|
||||||
|
const cheight = canvas.value.height;
|
||||||
|
const offsetX = props.offsetX;
|
||||||
|
const offsetY = props.offsetY;
|
||||||
|
const scaleX = ((cwidth / image.width) * (props.scale / 100)) / 5;
|
||||||
|
const scaleY = ((cheight / image.height) * (props.scale / 100)) / 5;
|
||||||
|
const scale = cwidth > cheight ? scaleX : scaleY;
|
||||||
|
const angle = props.angle;
|
||||||
|
const gapX = props.gapX;
|
||||||
|
const gapY = props.gapY;
|
||||||
|
const patternTransform = fabric.util.composeMatrix({
|
||||||
|
scaleX: scale,
|
||||||
|
scaleY: scale,
|
||||||
|
angle: angle,
|
||||||
|
});
|
||||||
|
// 创建透明 Canvas
|
||||||
|
const tcanvas = document.createElement("canvas");
|
||||||
|
tcanvas.width = image.width + gapX;
|
||||||
|
tcanvas.height = image.height + gapY;
|
||||||
|
const ctx = tcanvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
||||||
|
ctx.drawImage(image, 0, 0);
|
||||||
|
const pattern = new fabric.Pattern({
|
||||||
|
source: tcanvas,
|
||||||
|
repeat: "repeat",
|
||||||
|
patternTransform,
|
||||||
|
offsetX, // 水平偏移
|
||||||
|
offsetY, // 垂直偏移
|
||||||
|
});
|
||||||
|
const rect = new fabric.Rect({
|
||||||
|
id,
|
||||||
|
width: cwidth,
|
||||||
|
height: cheight,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
// scaleX: 1,
|
||||||
|
// scaleY: 1,
|
||||||
|
fill: pattern,
|
||||||
|
evented: false,
|
||||||
|
selectable: false,
|
||||||
|
});
|
||||||
|
canvas.value.add(rect);
|
||||||
|
canvas.value.renderAll();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='less' scoped>
|
||||||
|
.pingpu {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,54 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="modalTest"></div>
|
<div class="test">
|
||||||
<a-modal class="modal_test generalModel"
|
<div class="control">
|
||||||
v-model:visible="testModal"
|
<div>
|
||||||
:footer="null"
|
<span>偏移X</span>
|
||||||
:get-container="() => $refs.modalTest"
|
<input type="number" v-model="data.offsetX" />
|
||||||
width="78%"
|
</div>
|
||||||
:maskClosable="false"
|
<div>
|
||||||
:centered="true"
|
<span>偏移Y</span>
|
||||||
:closable="false"
|
<input type="number" v-model="data.offsetY" />
|
||||||
wrapClassName="#app"
|
</div>
|
||||||
:keyboard="false"
|
<div>
|
||||||
>
|
<span>角度</span>
|
||||||
<div class="generalModel_btn">
|
<input type="number" v-model="data.angle" />
|
||||||
<div class="generalModel_closeIcon" @click.stop="cancelDsign()">
|
</div>
|
||||||
<svg width="100%" height="100%" viewBox="0 0 46 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<div>
|
||||||
<circle cx="23" cy="23" r="23" fill="#000" fill-opacity="0.3"/>
|
<span>缩放</span>
|
||||||
<rect x="32.5063" y="12" width="3" height="29" rx="1.5" transform="rotate(45 32.5063 12)" fill="white"/>
|
<input type="number" v-model="data.scale" />
|
||||||
<rect x="34.6274" y="32.5059" width="3" height="29" rx="1.5" transform="rotate(135 34.6274 32.5059)" fill="white"/>
|
</div>
|
||||||
</svg>
|
<div>
|
||||||
|
<span>水平间距</span>
|
||||||
|
<input type="number" v-model="data.gapX" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>垂直间距</span>
|
||||||
|
<input type="number" v-model="data.gapY" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<canvasIndex></canvasIndex>
|
<div class="box">
|
||||||
</a-modal>
|
<pingpu v-bind="data" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent,ref,createVNode,nextTick} from 'vue'
|
import pingpu from "./pingpu.vue";
|
||||||
import canvasIndex from './index.vue'
|
const data = ref({
|
||||||
export default defineComponent({
|
url: "/src/assets/images/canvas/yinhua1.jpg",
|
||||||
components:{canvasIndex},
|
offsetX: 0, // px
|
||||||
setup(){
|
offsetY: 0, // px
|
||||||
let testModal = ref(true)
|
angle: 0, // 角度
|
||||||
const init = ()=>{
|
scale: 100,// %
|
||||||
testModal.value = true
|
gapX: 0, // px
|
||||||
}
|
gapY: 0, // px
|
||||||
const cancelDsign = ()=>{
|
|
||||||
testModal.value = false
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
testModal,
|
|
||||||
init,
|
|
||||||
cancelDsign,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
window.data = data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang='less' scoped>
|
<style lang='less' scoped>
|
||||||
.box {
|
.test {
|
||||||
border: 1px solid #f00;
|
> .control {
|
||||||
}
|
margin-left: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
> span {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
>input{
|
||||||
|
padding-left: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .box {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<model
|
<model
|
||||||
ref="model"
|
ref="model"
|
||||||
:key="positionKey"
|
:key="positionKey"
|
||||||
|
@addDetail="addDetail"
|
||||||
@canvasReload="canvasReload"
|
@canvasReload="canvasReload"
|
||||||
@detailEdit="detailEdit"
|
@detailEdit="detailEdit"
|
||||||
@addSketch="()=>isEditPattern.value = ''"
|
@addSketch="()=>isEditPattern.value = ''"
|
||||||
@@ -78,7 +78,16 @@
|
|||||||
<div class="item detailRight" :class="{canvas:isEditPattern.value}">
|
<div class="item detailRight" :class="{canvas:isEditPattern.value}">
|
||||||
<div class="submit">
|
<div class="submit">
|
||||||
</div>
|
</div>
|
||||||
<div class="contentRight" v-if="currentDetailType && !isEditPattern.value">
|
<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">
|
||||||
<detailRight ref="detailRight"></detailRight>
|
<detailRight ref="detailRight"></detailRight>
|
||||||
<div class="btn"
|
<div class="btn"
|
||||||
v-show="
|
v-show="
|
||||||
@@ -102,7 +111,7 @@
|
|||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<addDetails ref="addDetails" @setSloganData="setSloganData"></addDetails>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<div class="mark_loading" v-show="loadingShow">
|
<div class="mark_loading" v-show="loadingShow">
|
||||||
<a-spin size="large" />
|
<a-spin size="large" />
|
||||||
@@ -128,17 +137,18 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import addDetails from '@/component/Detail/addDetails.vue'
|
import addDetails from '@/component/Detail/addDetails.vue'
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components:{
|
components:{
|
||||||
detailLeft,model,detailRight,canvasBox
|
detailLeft,model,detailRight,canvasBox,addDetails
|
||||||
},
|
},
|
||||||
emits:['destroy'],
|
emits:['destroy'],
|
||||||
setup(props,{emit}) {
|
setup(props,{emit}) {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
const {locale} = useI18n()
|
||||||
const detailDom = reactive({
|
const detailDom = reactive({
|
||||||
model:null,
|
model:null,
|
||||||
canvasBox,
|
canvasBox,
|
||||||
detailRight,
|
detailRight,
|
||||||
detailLeft:null as any,
|
detailLeft:null as any,
|
||||||
|
addDetails:null as any,
|
||||||
})
|
})
|
||||||
const userDetail = computed(()=>{
|
const userDetail = computed(()=>{
|
||||||
return store.state.UserHabit.userDetail
|
return store.state.UserHabit.userDetail
|
||||||
@@ -506,6 +516,17 @@ export default defineComponent({
|
|||||||
sessionStorage.setItem('revocation', JSON.stringify(revocation));
|
sessionStorage.setItem('revocation', JSON.stringify(revocation));
|
||||||
sessionStorage.setItem('oppositeRevocation',JSON.stringify([]));
|
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(()=>{
|
onMounted(()=>{
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
})
|
})
|
||||||
@@ -518,6 +539,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return{
|
return{
|
||||||
|
locale,
|
||||||
...toRefs(detailDom),
|
...toRefs(detailDom),
|
||||||
...toRefs(detailData),
|
...toRefs(detailData),
|
||||||
closeModal,
|
closeModal,
|
||||||
@@ -531,6 +553,8 @@ export default defineComponent({
|
|||||||
canvasReload,
|
canvasReload,
|
||||||
modelOnLoad,
|
modelOnLoad,
|
||||||
sketchSysToLibrary,
|
sketchSysToLibrary,
|
||||||
|
addDetail,
|
||||||
|
setSloganData,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,17 @@
|
|||||||
<div class="canvasContent" ref="canvasContent">
|
<div class="canvasContent" ref="canvasContent">
|
||||||
<div class="content-bottom" ref="canvasContent">
|
<div class="content-bottom" ref="canvasContent">
|
||||||
<div class="contet">
|
<div class="contet">
|
||||||
|
<!-- :clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path" -->
|
||||||
<div class="canvas" v-if="currentView === 'canvasEditor'" @click.stop>
|
<div class="canvas" v-if="currentView === 'canvasEditor'" @click.stop>
|
||||||
<editCanvas v-if="canvasLoad" :config="canvasConfig"
|
<editCanvas v-if="canvasLoad" :config="canvasConfig"
|
||||||
@canvasInit="canvasInit"
|
@canvasInit="canvasInit"
|
||||||
@changeCanvas="changeCanvas"
|
@changeCanvas="changeCanvas"
|
||||||
is-edit
|
is-edit
|
||||||
:clothingImageUrl="selectDetail?.undividedLayerWithSinglePrint || selectDetail.undividedLayer || selectDetail.path"
|
:clothingImageUrl="selectDetail.path"
|
||||||
|
:clothingImageUrl2="selectDetail.undividedLayer"
|
||||||
showFixedLayer
|
showFixedLayer
|
||||||
:canvasJSON="canvasJSON"
|
:canvasJSON="canvasJSON"
|
||||||
|
:otherData="otherData"
|
||||||
:clothing-image-opts="{
|
:clothing-image-opts="{
|
||||||
imageMode:'contains',
|
imageMode:'contains',
|
||||||
}"
|
}"
|
||||||
@@ -108,6 +111,12 @@ export default defineComponent({
|
|||||||
canvasInstance:null as any,
|
canvasInstance:null as any,
|
||||||
canvasJSON:'',
|
canvasJSON:'',
|
||||||
hideCanvas: computed(()=>store.state.Workspace.projectPath !== route.fullPath),
|
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)=>{
|
watch(()=>detailData.selectDetail,(newValue,oldValue)=>{
|
||||||
detailData.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
detailData.imgDomIndex = detailData.frontBack.front.findIndex((item:any)=>item.id == newValue.id)
|
||||||
@@ -117,7 +126,6 @@ export default defineComponent({
|
|||||||
provide('canvasType',detailData.canvasType)
|
provide('canvasType',detailData.canvasType)
|
||||||
|
|
||||||
const editFront = (str:any)=>{//编辑前后片
|
const editFront = (str:any)=>{//编辑前后片
|
||||||
|
|
||||||
let canvasJSON = '' as any
|
let canvasJSON = '' as any
|
||||||
if(detailData.currentView === 'canvasEditor'){
|
if(detailData.currentView === 'canvasEditor'){
|
||||||
sessionStorage.setItem('sketchEdit',detailDom.editCanvas.getJSON())
|
sessionStorage.setItem('sketchEdit',detailDom.editCanvas.getJSON())
|
||||||
@@ -230,7 +238,7 @@ export default defineComponent({
|
|||||||
let size = {
|
let size = {
|
||||||
...detailData.canvasConfig,
|
...detailData.canvasConfig,
|
||||||
}
|
}
|
||||||
|
store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
|
||||||
segmentImage(value,full,size).then(async (rv)=>{
|
segmentImage(value,full,size).then(async (rv)=>{
|
||||||
let front = detailData.frontBack.front[detailData.imgDomIndex]
|
let front = detailData.frontBack.front[detailData.imgDomIndex]
|
||||||
let back = detailData.frontBack.back[detailData.imgDomIndex]
|
let back = detailData.frontBack.back[detailData.imgDomIndex]
|
||||||
@@ -243,7 +251,7 @@ export default defineComponent({
|
|||||||
let base64 = await resizeImageWithNativeCanvas(front.oldMaskUrl,value)
|
let base64 = await resizeImageWithNativeCanvas(front.oldMaskUrl,value)
|
||||||
front.maskUrl = base64
|
front.maskUrl = base64
|
||||||
back.imageUrl = rv.targetBackUrl
|
back.imageUrl = rv.targetBackUrl
|
||||||
store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
|
// store.commit('DesignDetail/updataDetailItem',{maskUrl:value})
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -309,7 +317,7 @@ export default defineComponent({
|
|||||||
sessionStorage.removeItem('frontBackEdit');
|
sessionStorage.removeItem('frontBackEdit');
|
||||||
sessionStorage.removeItem('sketchEdit');
|
sessionStorage.removeItem('sketchEdit');
|
||||||
detailData.canvasLoad = false
|
detailData.canvasLoad = false
|
||||||
privewDetail()
|
// privewDetail()
|
||||||
})
|
})
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
nextTick(async ()=>{
|
nextTick(async ()=>{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pallet">
|
<div class="pallet" ref="palletRef">
|
||||||
<div class="palletColo" @click="openPallet">
|
<div class="palletColo" @click="openPallet">
|
||||||
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
<div v-show="!selectColor.gradient" class="palletBackColor" :title="selectColor.name" :style="{'background-color':selectColor.hex}">
|
||||||
{{ selectColor.hex }}
|
{{ selectColor.hex }}
|
||||||
@@ -117,6 +117,7 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
const getpalletListDom = reactive({
|
const getpalletListDom = reactive({
|
||||||
})
|
})
|
||||||
|
const palletRef = ref(null)
|
||||||
watch(()=>palletData.color_,(newVal:any)=>{
|
watch(()=>palletData.color_,(newVal:any)=>{
|
||||||
if(!newVal?.rgba?.r)return
|
if(!newVal?.rgba?.r)return
|
||||||
if(palletData.color?.gradient?.gradientShow){
|
if(palletData.color?.gradient?.gradientShow){
|
||||||
@@ -221,7 +222,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
|
|
||||||
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
// this.selectColor = {rgba:gradientRgba,hex:hex} //顔色选择器默认颜色
|
||||||
let gradientWidth = (document.querySelector('.pallet .color_setting_operate_bg') as any).clientWidth
|
let gradientWidth = (palletRef.value.querySelector('.color_setting_operate_bg') as any).clientWidth
|
||||||
let position = {
|
let position = {
|
||||||
x:event.clientX,
|
x:event.clientX,
|
||||||
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
left:event.target.style.left?event.target.style.left.split('%')[0]:0
|
||||||
@@ -276,8 +277,8 @@ export default defineComponent({
|
|||||||
// 点击外部区域关闭颜色选择器
|
// 点击外部区域关闭颜色选择器
|
||||||
const handleClickOutside = (event: Event) => {
|
const handleClickOutside = (event: Event) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const colorSettingBlock = document.querySelector('.color_setting_block');
|
const colorSettingBlock = palletRef.value.querySelector('.color_setting_block');
|
||||||
const palletColo = document.querySelector('.palletColo');
|
const palletColo = palletRef.value.querySelector('.palletColo');
|
||||||
|
|
||||||
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
// 如果点击的是 .palletColo 或 .color_setting_block 内部,则不关闭
|
||||||
if (palletData.palletShow && colorSettingBlock &&
|
if (palletData.palletShow && colorSettingBlock &&
|
||||||
@@ -294,7 +295,7 @@ export default defineComponent({
|
|||||||
nextTick().then(()=>{
|
nextTick().then(()=>{
|
||||||
const backIcon = document.createElement('div');
|
const backIcon = document.createElement('div');
|
||||||
backIcon.classList.add('vc-sketch-color-wrap')
|
backIcon.classList.add('vc-sketch-color-wrap')
|
||||||
let dropperDom = document.getElementsByClassName("pallet")?.[0]?.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
let dropperDom = palletRef.value.getElementsByClassName('vc-chrome-fields-wrap')[0]
|
||||||
dropperDom.appendChild(backIcon);
|
dropperDom.appendChild(backIcon);
|
||||||
backIcon.addEventListener('click',async ()=>{
|
backIcon.addEventListener('click',async ()=>{
|
||||||
try {
|
try {
|
||||||
@@ -322,7 +323,7 @@ export default defineComponent({
|
|||||||
return{
|
return{
|
||||||
...toRefs(palletData),
|
...toRefs(palletData),
|
||||||
...toRefs(getpalletListDom),
|
...toRefs(getpalletListDom),
|
||||||
|
palletRef,
|
||||||
openPallet,
|
openPallet,
|
||||||
selectImgItem,
|
selectImgItem,
|
||||||
setOperate,
|
setOperate,
|
||||||
@@ -614,6 +615,7 @@ export default defineComponent({
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: "";
|
||||||
top: 0.2rem;
|
top: 0.2rem;
|
||||||
|
left: 0;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
<element v-show="currentDetailType == 'element'"></element>
|
<element v-show="currentDetailType == 'element'"></element>
|
||||||
<accessory v-show="currentDetailType == 'accessory'"></accessory>
|
<accessory v-show="currentDetailType == 'accessory'"></accessory>
|
||||||
<models v-show="currentDetailType == 'models'"></models>
|
<models v-show="currentDetailType == 'models'"></models>
|
||||||
<addDetails ref="addDetails" @setSloganData="setSloganData"></addDetails>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -22,12 +21,12 @@ import color from './colorBox/index.vue'
|
|||||||
import element from './element.vue'
|
import element from './element.vue'
|
||||||
import accessory from './accessory.vue'
|
import accessory from './accessory.vue'
|
||||||
import models from './models.vue'
|
import models from './models.vue'
|
||||||
import addDetails from '@/component/Detail/addDetails.vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components:{
|
components:{
|
||||||
sketch,print,color,addDetails,element,models,accessory
|
sketch,print,color,element,models,accessory
|
||||||
},
|
},
|
||||||
|
emit:['addDetail'],
|
||||||
setup(props,{emit}) {
|
setup(props,{emit}) {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const detailData = reactive({
|
const detailData = reactive({
|
||||||
@@ -45,14 +44,7 @@ export default defineComponent({
|
|||||||
sketch:null as any,
|
sketch:null as any,
|
||||||
})
|
})
|
||||||
const addDetail = () =>{
|
const addDetail = () =>{
|
||||||
let addDetails:any = getDetailListDom.addDetails
|
emit('addDetail')
|
||||||
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
|
const sketchSysToLibrary = ()=>{//系统sketch添加到library更新library
|
||||||
getDetailListDom.sketch.sketchSysToLibrary()
|
getDetailListDom.sketch.sketchSysToLibrary()
|
||||||
@@ -63,7 +55,6 @@ export default defineComponent({
|
|||||||
...toRefs(getDetailListData),
|
...toRefs(getDetailListData),
|
||||||
...toRefs(getDetailListDom),
|
...toRefs(getDetailListDom),
|
||||||
addDetail,
|
addDetail,
|
||||||
setSloganData,
|
|
||||||
sketchSysToLibrary,
|
sketchSysToLibrary,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- <img :src="selectDetail?.sketchString?selectDetail?.sketchString:selectDetail.path" alt=""> -->
|
<!-- <img :src="selectDetail?.sketchString?selectDetail?.sketchString:selectDetail.path" alt=""> -->
|
||||||
<img :src="selectDetail.path" alt="">
|
<img :src="selectDetail.path" alt="">
|
||||||
<!-- <img :src="selectDetail.sketchString || 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>
|
||||||
<div class="select_sketch" v-else>
|
<div class="select_sketch" v-else>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||