mirror of
https://github.com/palxiao/poster-design.git
synced 2025-06-08 03:19:59 +08:00
init
This commit is contained in:
parent
3b2bb39c2e
commit
c25bd8b3b0
40
.eslintrc.js
Normal file
40
.eslintrc.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-07-13 18:46:45
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2021-07-30 14:11:34
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
'alloy',
|
||||||
|
'alloy/vue',
|
||||||
|
// 'alloy/typescript',
|
||||||
|
'@vue/typescript',
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
// 你的环境变量(包含多个预定义的全局变量)
|
||||||
|
//
|
||||||
|
// browser: true,
|
||||||
|
// node: true,
|
||||||
|
// mocha: true,
|
||||||
|
// jest: true,
|
||||||
|
// jquery: true
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
// 你的全局变量(设置为 false 表示它不允许被重新赋值)
|
||||||
|
//
|
||||||
|
// myGlobal: false
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// 自定义你的规则
|
||||||
|
'vue/component-tags-order': ['off'],
|
||||||
|
'vue/no-multiple-template-root': ['off'],
|
||||||
|
// 'no-undef': 'off', // 禁止使用未定义的变量,会把TS声明视为变量,暂时关闭
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
legacyDecorators: true, // 配置允许注解
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
config.json
|
||||||
|
|
||||||
|
screenshot/node_modules/
|
||||||
|
screenshot/dist/
|
||||||
|
screenshot/_apidoc/
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
43
.prettierrc.js
Normal file
43
.prettierrc.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// .prettierrc.js 代码美化规则配置
|
||||||
|
module.exports = {
|
||||||
|
// 一行最多 n 字符
|
||||||
|
printWidth: 1000,
|
||||||
|
// 使用 2 个空格缩进
|
||||||
|
tabWidth: 2,
|
||||||
|
// 不使用缩进符,而使用空格
|
||||||
|
useTabs: false,
|
||||||
|
// 行尾需要有分号
|
||||||
|
semi: false,
|
||||||
|
// 使用单引号
|
||||||
|
singleQuote: true,
|
||||||
|
// 对象的 key 仅在必要时用引号
|
||||||
|
quoteProps: 'as-needed',
|
||||||
|
// jsx 不使用单引号,而使用双引号
|
||||||
|
jsxSingleQuote: false,
|
||||||
|
// 末尾需要有逗号
|
||||||
|
trailingComma: 'all',
|
||||||
|
// 大括号内的首尾需要空格
|
||||||
|
bracketSpacing: true,
|
||||||
|
// jsx 标签的反尖括号需要换行
|
||||||
|
jsxBracketSameLine: false,
|
||||||
|
// 箭头函数,只有一个参数的时候,也需要括号
|
||||||
|
arrowParens: 'always',
|
||||||
|
// 每个文件格式化的范围是文件的全部内容
|
||||||
|
rangeStart: 0,
|
||||||
|
rangeEnd: Infinity,
|
||||||
|
// 不需要写文件开头的 @prettier
|
||||||
|
requirePragma: false,
|
||||||
|
// 不需要自动在文件开头插入 @prettier
|
||||||
|
insertPragma: false,
|
||||||
|
// 使用默认的折行标准
|
||||||
|
proseWrap: 'preserve',
|
||||||
|
// 根据显示样式决定 html 要不要折行
|
||||||
|
htmlWhitespaceSensitivity: 'css',
|
||||||
|
// vue 文件中的 script 和 style 内不用缩进
|
||||||
|
vueIndentScriptAndStyle: false,
|
||||||
|
// 换行符使用 lf
|
||||||
|
endOfLine: 'lf',
|
||||||
|
// 格式化嵌入的内容
|
||||||
|
embeddedLanguageFormatting: 'auto',
|
||||||
|
}
|
||||||
|
|
211
README.md
211
README.md
@ -1,5 +1,212 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-14 10:44:31
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-18 15:01:01
|
||||||
|
-->
|
||||||
|
|
||||||
|
[在线体验](https://design.palxp.com/) | [中文文档](https://xp.palxp.com/)
|
||||||
|
|
||||||
## 迅排设计
|
## 迅排设计
|
||||||
|
|
||||||
一款漂亮的简易在线海报设计器,仿稿定设计。
|
一款漂亮且功能强大的在线海报图片设计器,仿稿定设计。
|
||||||
|
|
||||||
重构版本7月份开源,敬请期待~
|
让你轻松实现创意,迅速进行排版,感受云上设计带来的乐趣!
|
||||||
|
|
||||||
|
> 环境需求:**Node.js v16** 以上版本
|
||||||
|
|
||||||
|
### 拉取源码
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/palxiao/poster-design.git
|
||||||
|
cd poster-design
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run prepared
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本地运行
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
> 将会同时运行前端界面与图片生成服务:
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
|
||||||
|
### 运行结果
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
合成图片时本地会启动一个 Chrome 浏览器实例。
|
||||||
|
|
||||||
|
### 打包前端页面
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run v-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包图片生成服务
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sreenshot
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务端
|
||||||
|
|
||||||
|
可参考接口 API 文档自行实现服务端。
|
||||||
|
|
||||||
|
### 服务器配置
|
||||||
|
|
||||||
|
在 Linux 环境下,npm 自动安装的 Chrome 浏览器有可能会出错,所以推荐从外部安装好浏览器,再给项目中的配置文件 `config.js` 设置好路径,例如:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.executablePath = '/opt/google/chrome-unstable/chrome'
|
||||||
|
```
|
||||||
|
|
||||||
|
一些可能用到的 linux 命令参考:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
google-chrome --version # 查看浏览器版本号
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y google-chrome-stable // 安装最新稳定版谷歌浏览器
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker 容器
|
||||||
|
|
||||||
|
可以通过 Docker 运行一个带 Linux 浏览器的容器,然后暴露一个截图服务以供使用,我所使用的基础镜像为:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker pull howard86/puppeteer_node:12
|
||||||
|
```
|
||||||
|
|
||||||
|
运行容器命令参考(其中映射了 `/cache` 为临时目录,放生成图片用):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -itd -v /data/docker-home:/home -v /data/cache:/cache -p 7001:7001 --name screenshot howard86/puppeteer_node:12
|
||||||
|
```
|
||||||
|
|
||||||
|
运行后可以手动进入容器中查看谷歌浏览器版本,看需不需要升级,然后安装 pm2 作为服务管理工具,服务启动/重部署相关脚本命令可参考:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 delete screenshot-service'
|
||||||
|
docker exec screenshot /bin/bash -c 'cd /home/ && yarn'
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 start /home/screenshot-service.js'
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 flush'
|
||||||
|
```
|
||||||
|
|
||||||
|
如果不想像上面这样直接操作容器,可以在本地/服务器先运行镜像,进入容器中照例配置好 pm2,然后把该容器导出为新的镜像,例如:new-design/screenshot,命令运行参考:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -itd -u root -v ~/data/tmp/screenshot:/cache -p 9001:9001 --name screenshot2 new-design/screenshot /bin/sh -c "/usr/local/bin/pm2 start /home/dist/server.js && /usr/local/bin/pm2 flush"
|
||||||
|
```
|
||||||
|
|
||||||
|
这种方式只需要一个镜像以及一个启动命令即可部署,重新跑一遍命令也就相当于重启整个容器。> 环境需求:**Node.js v16** 以上版本
|
||||||
|
|
||||||
|
### 拉取源码
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/palxiao/poster-design.git
|
||||||
|
cd poster-design
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run prepared
|
||||||
|
```
|
||||||
|
|
||||||
|
### 本地运行
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
> 将会同时运行前端界面与图片生成服务:
|
||||||
|
>
|
||||||
|
> 
|
||||||
|
|
||||||
|
### 运行结果
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
合成图片时本地会启动一个 Chrome 浏览器实例。
|
||||||
|
|
||||||
|
### 打包前端页面
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run v-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包图片生成服务
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sreenshot
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务端
|
||||||
|
|
||||||
|
可参考接口 API 文档自行实现服务端。
|
||||||
|
|
||||||
|
### 服务器配置
|
||||||
|
|
||||||
|
在 Linux 环境下,npm 自动安装的 Chrome 浏览器有可能会出错,所以推荐从外部安装好浏览器,再给项目中的配置文件 `config.js` 设置好路径,例如:
|
||||||
|
|
||||||
|
```js
|
||||||
|
exports.executablePath = '/opt/google/chrome-unstable/chrome'
|
||||||
|
```
|
||||||
|
|
||||||
|
一些可能用到的 linux 命令参考:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
google-chrome --version # 查看浏览器版本号
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y google-chrome-stable // 安装最新稳定版谷歌浏览器
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker 容器
|
||||||
|
|
||||||
|
可以通过 Docker 运行一个带 Linux 浏览器的容器,然后暴露一个截图服务以供使用,我所使用的基础镜像为:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker pull howard86/puppeteer_node:12
|
||||||
|
```
|
||||||
|
|
||||||
|
运行容器命令参考(其中映射了 `/cache` 为临时目录,放生成图片用):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -itd -v /data/docker-home:/home -v /data/cache:/cache -p 7001:7001 --name screenshot howard86/puppeteer_node:12
|
||||||
|
```
|
||||||
|
|
||||||
|
运行后可以手动进入容器中查看谷歌浏览器版本,看需不需要升级,然后安装 pm2 作为服务管理工具,服务启动/重部署相关脚本命令可参考:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 delete screenshot-service'
|
||||||
|
docker exec screenshot /bin/bash -c 'cd /home/ && yarn'
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 start /home/screenshot-service.js'
|
||||||
|
docker exec screenshot /bin/bash -c 'pm2 flush'
|
||||||
|
```
|
||||||
|
|
||||||
|
如果不想像上面这样直接操作容器,可以在本地/服务器先运行镜像,进入容器中照例配置好 pm2,然后把该容器导出为新的镜像,例如:new-design/screenshot,命令运行参考:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -itd -u root -v ~/data/tmp/screenshot:/cache -p 9001:9001 --name screenshot2 new-design/screenshot /bin/sh -c "/usr/local/bin/pm2 start /home/dist/server.js && /usr/local/bin/pm2 flush"
|
||||||
|
```
|
||||||
|
|
||||||
|
这种方式只需要一个镜像以及一个启动命令即可部署,重新跑一遍命令也就相当于重启整个容器。
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
|
||||||
|
本项目使用或参考了以下优秀开源项目:
|
||||||
|
|
||||||
|
- [moveable](https://github.com/daybrush/moveable): 提供了画布中选择、拖动缩放等能力
|
||||||
|
- [html2canvas](https://github.com/niklasvh/html2canvas): 前端生成图片兜底方案
|
||||||
|
- [qr-code-styling](https://qr-code-styling.com/): 风格化二维码
|
||||||
|
- [sky](https://github.com/cfour-hi/sky): 参考了其 PSD 解析的实现
|
||||||
|
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
23
index.html
Normal file
23
index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-01-17 16:06:30
|
||||||
|
* @Description: Design Palxp
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-13 10:32:13
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>迅排设计 - 轻松创意,迅捷排版,感受云上设计带来的乐趣!</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<script defer src="/snap.svg-min.js"></script>
|
||||||
|
<!-- <script defer src="https://cdn.palxp.com/snap.svg-min.js"></script> -->
|
||||||
|
</body>
|
||||||
|
</html>
|
28999
package-lock.json
generated
Normal file
28999
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
package.json
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "xunpai-design",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"author": "ShawnPhang",
|
||||||
|
"scripts": {
|
||||||
|
"prepared": "npm i --force && cd screenshot && npm i",
|
||||||
|
"serve": "npm run dev & cd screenshot && npm run dev",
|
||||||
|
"dev": "cross-env NODE_ENV=development vite",
|
||||||
|
"v-build": "cross-env NODE_ENV=production && vite build",
|
||||||
|
"v-build-hard": "cross-env NODE_ENV=production vue-tsc --noEmit && vite build",
|
||||||
|
"build": "node script/set config.json && npm run v-build && sh script/reverse.sh",
|
||||||
|
"publish": "sh script/publish.sh",
|
||||||
|
"publish-fast": "git add . && git commit -m 'build: auto publish' && sh script/publish.sh"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@gradio/client": "^0.1.4",
|
||||||
|
"@palxp/color-picker": "^1.2.5",
|
||||||
|
"@scena/guides": "^0.18.1",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"element-plus": "^2.3.7",
|
||||||
|
"fontfaceobserver": "^2.1.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"moveable": "^0.26.0",
|
||||||
|
"moveable-helper": "^0.4.0",
|
||||||
|
"nanoid": "^3.1.23",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"qr-code-styling": "^1.6.0-rc.1",
|
||||||
|
"selecto": "^1.13.0",
|
||||||
|
"throttle-debounce": "^3.0.1",
|
||||||
|
"v-lazy-image": "^2.1.1",
|
||||||
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0-0",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
|
"vuex": "^4.0.0-0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.3.1",
|
||||||
|
"@types/throttle-debounce": "^2.1.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||||
|
"@typescript-eslint/parser": "^4.28.3",
|
||||||
|
"@vitejs/plugin-vue": "^1.2.4",
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"@vue/compiler-sfc": "^3.1.4",
|
||||||
|
"@vue/eslint-config-typescript": "^7.0.0",
|
||||||
|
"autoprefixer": "^10.3.1",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"esbuild-loader": "^2.13.1",
|
||||||
|
"eslint": "^7.29.0",
|
||||||
|
"eslint-config-alloy": "^4.1.0",
|
||||||
|
"eslint-plugin-vue": "^7.12.1",
|
||||||
|
"less": "^4.1.1",
|
||||||
|
"sass": "^1.63.6",
|
||||||
|
"typescript": "~4.1.5",
|
||||||
|
"unplugin-element-plus": "^0.7.1",
|
||||||
|
"vite": "^2.4.1",
|
||||||
|
"vite-plugin-compression": "^0.3.0",
|
||||||
|
"vue-cli-plugin-norm": "~1.2.2",
|
||||||
|
"vue-eslint-parser": "^7.6.0",
|
||||||
|
"vue-tsc": "^0.2.0"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
25
postcss.config.js
Normal file
25
postcss.config.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-10-11 14:00:18
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-26 20:48:18
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
// "postcss-import": {},
|
||||||
|
// "postcss-url": {},
|
||||||
|
// to edit target browsers: use "browserslist" field in package.json
|
||||||
|
autoprefixer: {
|
||||||
|
overrideBrowserslist: [
|
||||||
|
'Android 4.1',
|
||||||
|
'iOS 7.1',
|
||||||
|
'Chrome > 31',
|
||||||
|
'ff > 31',
|
||||||
|
'ie >= 8',
|
||||||
|
'last 10 versions', // 所有主流浏览器最近10版本用
|
||||||
|
],
|
||||||
|
grid: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg t="1647329483504" class="icon" viewBox="0 0 1260 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4962" width="32" height="32" data-spm-anchor-id="a313x.7781069.0.i13"><path d="M166.137 564.657a35.604 35.604 0 1 1 71.168 0 35.604 35.604 0 0 1-71.168 0z m54.745-179.83a55.847 55.847 0 1 1 111.695 0 55.847 55.847 0 0 1-111.656 0z m206.297-146.314a72.074 72.074 0 1 1 144.108 0 72.074 72.074 0 0 1-144.108 0z m272.541 0a90.466 90.466 0 1 1 180.972 0 90.466 90.466 0 0 1-180.972 0z m549.022-88.103a31.783 31.783 0 0 1 3.15 44.82L771.597 748.308l-108.74 75.658 60.612-119.139 479.39-552.093a31.783 31.783 0 0 1 44.859-3.151l0.945 0.787z m-82.157 307.003a32.414 32.414 0 0 1 32.296 34.973h0.118c-2.56 29.46-73.059 298.536-269.982 415.35-328.192 194.718-543.35 48.759-586.358-20.755-29.696-48.01-52.381-100.864-69.71-120.793-27.215-31.192-150.056 43.245-233.945-50.845-83.929-94.13-58.526-470.45 343.276-651.028 367.931-165.337 631.414 36.155 673.162 71.286a31.429 31.429 0 1 1-38.99 49.35C917.241 102.4 719.53 28.198 487.357 89.796c-331.815 86.41-465.683 435.554-408.97 570.25 30.523 72.467 175.853 6.223 233.315 53.996 21.425 17.802 59.037 91.254 83.732 134.774 44.898 79.281 286.72 140.997 497.979 3.82 195.82-127.133 240.718-365.331 240.718-365.331h0.119a32.414 32.414 0 0 1 32.295-29.893z" fill="#1195db" p-id="4963" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
32082
public/psd.js
Normal file
32082
public/psd.js
Normal file
File diff suppressed because it is too large
Load Diff
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
21
public/snap.svg-min.js
vendored
Normal file
21
public/snap.svg-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
70
screenshot/.gitignore
vendored
Normal file
70
screenshot/.gitignore
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# 创建自己的业务配置,不提交到库
|
||||||
|
# config.*
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
static
|
||||||
|
config.json
|
||||||
|
config.js
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
jspm_packages/
|
||||||
|
_apidoc/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
21
screenshot/README.md
Normal file
21
screenshot/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-01 13:41:59
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-04 19:04:44
|
||||||
|
-->
|
||||||
|
# Node截图服务
|
||||||
|
|
||||||
|
### npm run dev
|
||||||
|
|
||||||
|
ts-node 直接运行,并监听文件修改自动热重启
|
||||||
|
|
||||||
|
### npm run build
|
||||||
|
|
||||||
|
使用 webpack 打包文件
|
||||||
|
|
||||||
|
### npm run serve (不使用)
|
||||||
|
|
||||||
|
webpack/tsc 编译 ts,supervisor/pm2 监听编译后文件变动重启服务,gulp 自动化任务
|
||||||
|
|
30
screenshot/gulpfile.js
Normal file
30
screenshot/gulpfile.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
var gulp = require('gulp');
|
||||||
|
var exec = require('child_process').exec;
|
||||||
|
var spawn = require('child_process').spawn;
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
gulp.task('clean', function() {
|
||||||
|
return spawn('rm', ['-rf', path.join(__dirname, 'dist')])
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('build-ts', function(){
|
||||||
|
return exec('webpack --watch',(error,stdout,stderr)=>{
|
||||||
|
console.log(`build ts====>stdout: ${stdout}`);
|
||||||
|
console.log(`build ts====>stderr: ${stderr}`);
|
||||||
|
if (error !== null) {
|
||||||
|
console.log(`exec error: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//自动重启服务器
|
||||||
|
gulp.task('restart',function(){
|
||||||
|
return exec('supervisor -w dist ./dist/main.js',(error,stdout,stderr)=>{
|
||||||
|
console.log(`restart=====>stdout: ${stdout}`);
|
||||||
|
console.log(`restart=====>stderr: ${stderr}`);
|
||||||
|
if (error !== null) {
|
||||||
|
console.log(`exec error: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default',gulp.series('clean',gulp.parallel('build-ts','restart')));
|
53
screenshot/package.json
Normal file
53
screenshot/package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "screenshot-node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/main",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "ts-node-dev src/main",
|
||||||
|
"test": "cd ./static && hs --cors",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
|
"serve": "ts-node src/main",
|
||||||
|
"serve-test": "gulp",
|
||||||
|
"start": "webpack --watch",
|
||||||
|
"serverstart": "pm2 start ./dist/server.js --watch",
|
||||||
|
"tscstart": "tsc -w",
|
||||||
|
"serverstart2": "supervisor -w www ./www/main.js",
|
||||||
|
"build:apidoc": "apidoc -i src/ -o _apidoc/",
|
||||||
|
"publish": "rm -rf ./static && rm -rf ./_apidoc && sh script/publish.sh",
|
||||||
|
"publish-fast": "git add . && git commit -m 'build: auto publish' && npm run publish"
|
||||||
|
},
|
||||||
|
"author": "ShawnPhang",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"gif-encoder-2": "^1.0.5",
|
||||||
|
"images": "^3.2.4",
|
||||||
|
"png-js": "^1.0.0",
|
||||||
|
"puppeteer": "^10.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^12.6.9",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"express-graphql": "^0.9.0",
|
||||||
|
"graphql": "^14.5.4",
|
||||||
|
"gulp": "^4.0.2",
|
||||||
|
"http-server": "^14.0.0",
|
||||||
|
"speed-measure-webpack-plugin": "^1.5.0",
|
||||||
|
"supervisor": "^0.12.0",
|
||||||
|
"ts-loader": "^6.0.4",
|
||||||
|
"ts-node": "^8.3.0",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.40",
|
||||||
|
"typescript": "^3.5.3",
|
||||||
|
"webpack": "^4.39.1",
|
||||||
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
|
"webpack-cli": "^3.3.6",
|
||||||
|
"webpack-node-externals": "^3.0.0"
|
||||||
|
},
|
||||||
|
"apidoc": {
|
||||||
|
"title": "自动api接口文档",
|
||||||
|
"url": "http://localhost:9999/",
|
||||||
|
"sampleUrl": "http://localhost:9999/"
|
||||||
|
}
|
||||||
|
}
|
18
screenshot/package_build.json
Normal file
18
screenshot/package_build.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "screenshot-node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/main",
|
||||||
|
"scripts": {},
|
||||||
|
"author": "ShawnPhang",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"puppeteer": "^10.4.0",
|
||||||
|
"images": "3.2.3",
|
||||||
|
"png-js": "^1.0.0",
|
||||||
|
"gif-encoder-2": "^1.0.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
36
screenshot/src/configs.ts
Normal file
36
screenshot/src/configs.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-01 13:41:59
|
||||||
|
* @Description: 配置文件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-06 14:53:05
|
||||||
|
*/
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
exports.servicePort = 7001
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置服务器端的chrome浏览器位置
|
||||||
|
*/
|
||||||
|
exports.executablePath = '/opt/google/chrome-unstable/chrome',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截图并发数上限
|
||||||
|
*/
|
||||||
|
exports.maxNum = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截图队列的阈值,超出时请求将会被熔断
|
||||||
|
*/
|
||||||
|
exports.upperLimit = 20
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多久释放浏览器驻留内存,单位:秒(多标签页版生效)
|
||||||
|
*/
|
||||||
|
exports.releaseTime = 300
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片缓存目录位置,根据实际情况调整
|
||||||
|
*/
|
||||||
|
exports.filePath = isDev ? process.cwd() + `/static/` : '/cache/'
|
||||||
|
// exports.filePath = process.cwd() + `/static/`
|
14
screenshot/src/control/api.ts
Normal file
14
screenshot/src/control/api.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2020-07-22 20:13:14
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-06 16:00:09
|
||||||
|
*/
|
||||||
|
let path = '/api';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GETIMAGE: path + '/get_img',
|
||||||
|
SCREENGHOT: path + '/screenshots',
|
||||||
|
PRINTSCREEN: path + '/printscreen'
|
||||||
|
};
|
18
screenshot/src/control/router.ts
Normal file
18
screenshot/src/control/router.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2020-07-22 20:13:14
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-06 15:59:58
|
||||||
|
*/
|
||||||
|
const rExpress = require('express');
|
||||||
|
const screenshots = require('../service/screenshots.ts');
|
||||||
|
const api = require('./api.ts');
|
||||||
|
const rRouter = rExpress.Router();
|
||||||
|
|
||||||
|
rRouter.get(api.SCREENGHOT, screenshots.screenshots);
|
||||||
|
rRouter.get(api.PRINTSCREEN, screenshots.printscreen);
|
||||||
|
rRouter.get(api.GETIMAGE, screenshots.getImg);
|
||||||
|
|
||||||
|
module.exports = rRouter;
|
||||||
|
|
52
screenshot/src/main.ts
Normal file
52
screenshot/src/main.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-01 13:41:59
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-06 10:19:18
|
||||||
|
*/
|
||||||
|
const express = require('express')
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
const fs = require('fs')
|
||||||
|
// const path = require('path')
|
||||||
|
const router = require('./control/router.ts')
|
||||||
|
const { filePath, servicePort } = require('./configs.ts')
|
||||||
|
const handleTimeout = require('./utils/timeout.ts')
|
||||||
|
|
||||||
|
const port = process.env.PORT || servicePort
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
const createFolder = (folder: string) => {
|
||||||
|
try {
|
||||||
|
fs.accessSync(folder)
|
||||||
|
} catch (e) {
|
||||||
|
fs.mkdirSync(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createFolder(filePath)
|
||||||
|
|
||||||
|
app.all('*', (req: any, res: any, next: any) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization,Content-Length,Content-Size');
|
||||||
|
res.header('Access-Control-Allow-Methods', '*');
|
||||||
|
res.header('Content-Type', 'application/json;charset=utf-8');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/static', express.static('static'))
|
||||||
|
// app.use('/cache', express.static('cache'))
|
||||||
|
|
||||||
|
app.use(handleTimeout)
|
||||||
|
|
||||||
|
app.use((req: any, res: any, next: any) => {
|
||||||
|
console.log(req.path)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }))
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.listen(port, () => console.log(`devServer start on port:${port}`))
|
134
screenshot/src/service/screenshots.ts
Normal file
134
screenshot/src/service/screenshots.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2020-07-22 20:13:14
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-17 18:03:13
|
||||||
|
*/
|
||||||
|
const { saveScreenshot } = require('../utils/download-single.ts')
|
||||||
|
const { filePath, upperLimit } = require('../configs.ts')
|
||||||
|
const { queueRun, queueList } = require('../utils/node-queue.ts')
|
||||||
|
// const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async getImg(req: any, res: any) {
|
||||||
|
/**
|
||||||
|
* @api {get} api/get_img 获取图片
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup screenShot
|
||||||
|
*
|
||||||
|
* @apiParam {String|Number} id (必传) 截图id
|
||||||
|
* @apiParam {String} type 可选, file源文件,cover封面图
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
let { id, type = 'file' } = req.query
|
||||||
|
const path = filePath + `${id}-screenshot.png`
|
||||||
|
const thumbPath = type === 'cover' ? filePath + `${id}-cover.jpg` : null
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
fs.statSync(path)
|
||||||
|
res.setHeader('Content-Type', 'image/jpg')
|
||||||
|
type === 'file' ? res.sendFile(path) : res.sendFile(thumbPath)
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ code: 500, msg: '请求图片不存在' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.json({ code: 500, msg: '缺少参数,请检查' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async screenshots(req: any, res: any) {
|
||||||
|
/**
|
||||||
|
* @api {get} api/screenshots 截图
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup screenShot
|
||||||
|
*
|
||||||
|
* @apiParam {String|Number} id (可选) 截图id,高优先级
|
||||||
|
* @apiParam {String|Number} tempid (可选) 模板id,低优先级,无id时取该值
|
||||||
|
* @apiParam {String} width (必传)视窗大小
|
||||||
|
* @apiParam {String} height (必传)视窗大小
|
||||||
|
* @apiParam {String} screenshot_url 可选
|
||||||
|
* @apiParam {String} type 可选, file正常截图返回,cover封面生成,默认file
|
||||||
|
* @apiParam {String} size 可选, 按比例缩小到宽度
|
||||||
|
* @apiParam {String} quality 可选, 质量
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
let { id, tempid, width, height, screenshot_url, type = 'file', size, quality } = req.query
|
||||||
|
const defaultUrl = isDev ? 'http://localhost:3000/draw' : 'https://design.palxp.com/draw'
|
||||||
|
const url = (screenshot_url || defaultUrl) + `${id ? '?id=' : '?tempid='}`
|
||||||
|
id = id || '_' + tempid
|
||||||
|
const path = filePath + `${id}-screenshot.png`
|
||||||
|
const thumbPath = type === 'cover' ? filePath + `${id}-cover.jpg` : null
|
||||||
|
|
||||||
|
if (id && width && height) {
|
||||||
|
if (queueList.length > upperLimit) {
|
||||||
|
res.json({ code: 200, msg: '服务器表示顶不住啊,等等再来吧~' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// console.log(url + id, path, thumbPath);
|
||||||
|
queueRun(saveScreenshot, url + id, { width, height, path, thumbPath, size, quality })
|
||||||
|
.then(() => {
|
||||||
|
res.setHeader('Content-Type', 'image/jpg')
|
||||||
|
// const stats = fs.statSync(path)
|
||||||
|
// res.setHeader('Cache-Control', stats.size)
|
||||||
|
type === 'file' ? res.sendFile(path) : res.sendFile(thumbPath)
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
res.json({ code: 500, msg: '图片生成错误' })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.json({ code: 500, msg: '缺少参数,请检查' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async printscreen(req: any, res: any) {
|
||||||
|
/**
|
||||||
|
* @api {get} api/printscreen 全屏网页截图
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup screenShot
|
||||||
|
*
|
||||||
|
* @apiParam {String} url (必传) 目标网页link
|
||||||
|
* @apiParam {String} width (可选) 视窗大小
|
||||||
|
* @apiParam {String} height (可选) 视窗大小
|
||||||
|
* @apiParam {Boolean} prevent (可选, 默认false) true: 阻止立即截图,使用注入函数接管
|
||||||
|
* @apiParam {String} type (可选, 默认file) file: 返回二进制文件,cover: 立即返回地址(path, thumbPath)
|
||||||
|
* @apiParam {String} size 可选 (只在type=cover生效, eg:300,等比缩放到300像素宽)传该值时会额外产生小图(封面,格式jpeg)
|
||||||
|
* @apiParam {String} quality 可选 (只在有size生效, eg:75),压缩质量:1-100,质量越小图片占用空间越小
|
||||||
|
* @apiParam {Number} wait (可选) 截图前的等待时间,单位 ms
|
||||||
|
* @apiParam {String} ua (可选) 模拟设备 eg: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1'
|
||||||
|
* @apiParam {String} devices (可选) 套用设备预设,传该值则ua、width、height均会失效。eg: iPhone 6 所有预设:/src/utils/widget/Device.js
|
||||||
|
* @apiParam {Number} scale (可选) 针对移动端的设备像素比(DPR) 整型范围 1~4,默认1
|
||||||
|
*/
|
||||||
|
let { width = 375, height = 0, url, type = 'file', size, quality, prevent = false, ua, devices, scale, wait } = req.query
|
||||||
|
const path = filePath + `screenshot_${new Date().getTime()}.png`
|
||||||
|
const thumbPath = type === 'cover' ? path.replace('.png', '.jpg') : null
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
const sign = new Date().getTime() + ''
|
||||||
|
req._queueSign = sign
|
||||||
|
// console.log(url + id, path, thumbPath);
|
||||||
|
if (queueList.length > upperLimit) {
|
||||||
|
res.json({ code: 200, msg: '业务繁忙,等等再来吧~' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queueRun(saveScreenshot, url, { width, height, path, thumbPath, size, quality, prevent, ua, devices, scale, wait }, sign)
|
||||||
|
.then(() => {
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.setHeader('Content-Type', 'image/jpg')
|
||||||
|
// const stats = fs.statSync(path)
|
||||||
|
// res.setHeader('Cache-Control', stats.size)
|
||||||
|
type === 'file' ? res.sendFile(path) : res.sendFile(thumbPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
res.json({ code: 500, msg: '图片生成错误!' })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.json({ code: 500, msg: '缺少参数,请检查' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
6
screenshot/src/shims-my.d.ts
vendored
Normal file
6
screenshot/src/shims-my.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
declare namespace Type {
|
||||||
|
export interface Object {
|
||||||
|
[propName: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
122
screenshot/src/utils/download-single.ts
Normal file
122
screenshot/src/utils/download-single.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-30 14:47:22
|
||||||
|
* @Description: 下载图片(单浏览器版,适用于低配置服务器)
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-17 18:03:57
|
||||||
|
*/
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
const puppeteer = require('puppeteer')
|
||||||
|
const images = require('images')
|
||||||
|
const { executablePath } = require('../configs.ts')
|
||||||
|
const forceTimeOut = 60 // 强制超时时间,单位:秒
|
||||||
|
|
||||||
|
const saveScreenshot = async (url: string, { path, width, height, thumbPath, size = 0, quality = 0, prevent, ua, devices, scale, wait }: any) => {
|
||||||
|
return new Promise(async (resolve: Function) => {
|
||||||
|
// 启动浏览器
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: !isDev,
|
||||||
|
executablePath: isDev ? null : executablePath,
|
||||||
|
ignoreHTTPSErrors: true, // 忽略https安全提示
|
||||||
|
args: ['–no-first-run', '–single-process', '–disable-gpu', '–no-zygote', '–disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${width},${height}`], // 优化配置
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开页面
|
||||||
|
const page = await browser.newPage()
|
||||||
|
// 设置浏览器视窗
|
||||||
|
page.setViewport({
|
||||||
|
width: Number(width),
|
||||||
|
height: Number(height),
|
||||||
|
deviceScaleFactor: !isNaN(scale) ? (+scale > 4 ? 4 : +scale) : 1,
|
||||||
|
})
|
||||||
|
ua && page.setUserAgent(ua)
|
||||||
|
if (devices) {
|
||||||
|
devices = puppeteer.devices[devices]
|
||||||
|
devices && (await page.emulate(devices))
|
||||||
|
}
|
||||||
|
// 自动模式下页面加载完毕立即截图
|
||||||
|
if (prevent === false) {
|
||||||
|
page.on('load', async () => {
|
||||||
|
await autoScroll()
|
||||||
|
await sleep(wait)
|
||||||
|
// await waitTillHTMLRendered(page)
|
||||||
|
await page.screenshot({ path, fullPage: true })
|
||||||
|
// 关闭浏览器
|
||||||
|
await browser.close()
|
||||||
|
compress()
|
||||||
|
clearTimeout(regulators)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 主动模式下注入全局方法
|
||||||
|
await page.exposeFunction('loadFinishToInject', async () => {
|
||||||
|
// console.log('-> 开始截图')
|
||||||
|
await page.screenshot({ path })
|
||||||
|
// 关闭浏览器
|
||||||
|
await browser.close()
|
||||||
|
compress()
|
||||||
|
// console.log('浏览器已释放');
|
||||||
|
clearTimeout(regulators)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 地址栏输入网页地址
|
||||||
|
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
||||||
|
|
||||||
|
// 截图: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions
|
||||||
|
const regulators = setTimeout(() => {
|
||||||
|
browser && browser.close()
|
||||||
|
console.log('强制释放浏览器')
|
||||||
|
resolve()
|
||||||
|
}, forceTimeOut * 1000)
|
||||||
|
|
||||||
|
function compress() {
|
||||||
|
// 压缩图片
|
||||||
|
try {
|
||||||
|
thumbPath &&
|
||||||
|
images(path)
|
||||||
|
.size(+size || 300)
|
||||||
|
.save(thumbPath, { quality: +quality || 70 })
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoScroll() {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await new Promise((resolve: any, reject: any) => {
|
||||||
|
try {
|
||||||
|
const maxScroll = Number.MAX_SAFE_INTEGER
|
||||||
|
let lastScroll = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
window.scrollBy(0, 100)
|
||||||
|
const scrollTop = document.documentElement.scrollTop || window.scrollY
|
||||||
|
if (scrollTop === maxScroll || scrollTop === lastScroll) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
lastScroll = scrollTop
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(timeout: number = 1) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { saveScreenshot }
|
||||||
|
|
||||||
|
export {}
|
128
screenshot/src/utils/download.ts
Normal file
128
screenshot/src/utils/download.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-30 14:47:22
|
||||||
|
* @Description: 下载图片(多标签页版本,不建议在低配置服务器中使用)
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-17 18:05:31
|
||||||
|
*/
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
const puppeteer = require('puppeteer')
|
||||||
|
const images = require('images')
|
||||||
|
const { executablePath, releaseTime } = require('../configs.ts')
|
||||||
|
const forceTimeOut = 60 // 强制超时时间,单位:秒
|
||||||
|
let browser: typeof puppeteer = null
|
||||||
|
let release: any = null
|
||||||
|
|
||||||
|
const saveScreenshot = async (url: string, { path, width, height, thumbPath, size = 0, quality = 0, prevent, ua, devices, scale, wait }: any) => {
|
||||||
|
return new Promise(async (resolve: Function) => {
|
||||||
|
// 启动浏览器
|
||||||
|
if (!browser) {
|
||||||
|
browser = await puppeteer.launch({
|
||||||
|
headless: !isDev,
|
||||||
|
executablePath: isDev ? null : executablePath,
|
||||||
|
ignoreHTTPSErrors: true, // 忽略https安全提示
|
||||||
|
args: ['–no-first-run', '–single-process', '–disable-gpu', '–no-zygote', '–disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox', `--window-size=${width},${height}`], // 优化配置
|
||||||
|
defaultViewport: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开页面
|
||||||
|
const page = await browser.newPage()
|
||||||
|
// 设置浏览器视窗
|
||||||
|
page.setViewport({
|
||||||
|
width: Number(width),
|
||||||
|
height: Number(height),
|
||||||
|
deviceScaleFactor: !isNaN(scale) ? (+scale > 4 ? 4 : +scale) : 1,
|
||||||
|
})
|
||||||
|
ua && page.setUserAgent(ua)
|
||||||
|
if (devices) {
|
||||||
|
devices = puppeteer.devices[devices]
|
||||||
|
devices && (await page.emulate(devices))
|
||||||
|
}
|
||||||
|
// 自动模式下页面加载完毕立即截图
|
||||||
|
if (prevent === false) {
|
||||||
|
page.on('load', async () => {
|
||||||
|
clearTimeout(regulators)
|
||||||
|
await autoScroll(page)
|
||||||
|
await sleep(wait)
|
||||||
|
// await waitTillHTMLRendered(page)
|
||||||
|
await page.screenshot({ path, fullPage: true })
|
||||||
|
await page.close()
|
||||||
|
thumbPath && compress()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 主动模式下注入全局方法
|
||||||
|
await page.exposeFunction('loadFinishToInject', async () => {
|
||||||
|
clearTimeout(regulators)
|
||||||
|
await page.screenshot({ path }) // console.log('-> 开始截图')
|
||||||
|
await page.close()
|
||||||
|
thumbPath && compress()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地址栏输入网页地址
|
||||||
|
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
||||||
|
|
||||||
|
// 截图: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagescreenshotoptions
|
||||||
|
const regulators = setTimeout(() => {
|
||||||
|
page.close()
|
||||||
|
console.log('任务超时,已失败')
|
||||||
|
resolve()
|
||||||
|
}, forceTimeOut * 1000)
|
||||||
|
|
||||||
|
clearTimeout(release)
|
||||||
|
release = setTimeout(() => {
|
||||||
|
browser && browser.close()
|
||||||
|
browser = null
|
||||||
|
}, releaseTime * 1000);
|
||||||
|
|
||||||
|
function compress() {
|
||||||
|
// 压缩图片
|
||||||
|
try {
|
||||||
|
images(path)
|
||||||
|
.size(+size || 300)
|
||||||
|
.save(thumbPath, { quality: +quality || 70 })
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(timeout: number = 1) {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoScroll(page: any) {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await new Promise((resolve: any, reject: any) => {
|
||||||
|
try {
|
||||||
|
const maxScroll = Number.MAX_SAFE_INTEGER
|
||||||
|
let lastScroll = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
window.scrollBy(0, 100)
|
||||||
|
const scrollTop = document.documentElement.scrollTop || window.scrollY
|
||||||
|
if (scrollTop === maxScroll || scrollTop === lastScroll) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
lastScroll = scrollTop
|
||||||
|
}
|
||||||
|
}, 60)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { saveScreenshot }
|
||||||
|
|
||||||
|
export {}
|
41
screenshot/src/utils/node-queue.ts
Normal file
41
screenshot/src/utils/node-queue.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-12-24 18:09:35
|
||||||
|
* @Description: 异步队列
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-06 10:19:40
|
||||||
|
*/
|
||||||
|
interface Queue {
|
||||||
|
Fn: Function
|
||||||
|
sign?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
const { maxNum } = require('../configs.ts')
|
||||||
|
const queueList: any = [] // 任务队列
|
||||||
|
let curNum = 0 // 当前执行的任务数
|
||||||
|
|
||||||
|
function queueRun(business: Function, ...arg: any) {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const Fn = async () => resolve(await business(...arg))
|
||||||
|
const sign = { ...arg }[2]
|
||||||
|
if (curNum >= maxNum) {
|
||||||
|
queueList.push({ sign, Fn })
|
||||||
|
} else {
|
||||||
|
await run(Fn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(Fn: Function) {
|
||||||
|
curNum++
|
||||||
|
Fn().then((res: any) => {
|
||||||
|
curNum--
|
||||||
|
if (queueList.length > 0) {
|
||||||
|
const Task: Queue = queueList.shift()
|
||||||
|
run(Task.Fn)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { queueRun, queueList }
|
26
screenshot/src/utils/timeout.ts
Normal file
26
screenshot/src/utils/timeout.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-12-24 17:51:15
|
||||||
|
* @Description: 超时中间件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-05 20:17:00
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = async (req: any, res: any, next: any) => {
|
||||||
|
const { queueList } = require('../utils/node-queue.ts')
|
||||||
|
const time = 30000 // 设置所有HTTP请求的服务器响应超时时间
|
||||||
|
res.setTimeout(time, () => {
|
||||||
|
const statusCode = 408
|
||||||
|
const index = queueList.findIndex((x: any) => x.sign === req._queueSign)
|
||||||
|
if (index !== -1) {
|
||||||
|
queueList.splice(index, 1)
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(statusCode).json({
|
||||||
|
statusCode,
|
||||||
|
message: '响应超时,任务已取消,请重试',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
next()
|
||||||
|
}
|
76
screenshot/src/utils/widget/Device.js
Normal file
76
screenshot/src/utils/widget/Device.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-06-09 17:54:26
|
||||||
|
* @Description: 设备预设列表
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-28 00:22:30
|
||||||
|
*/
|
||||||
|
const DevicesNames = [
|
||||||
|
'Blackberry PlayBook',
|
||||||
|
'Blackberry PlayBook landscape',
|
||||||
|
'BlackBerry Z30',
|
||||||
|
'BlackBerry Z30 landscape',
|
||||||
|
'Galaxy Note 3',
|
||||||
|
'Galaxy Note 3 landscape',
|
||||||
|
'Galaxy Note II',
|
||||||
|
'Galaxy Note II landscape',
|
||||||
|
'Galaxy S III',
|
||||||
|
'Galaxy S III landscape',
|
||||||
|
'Galaxy S5',
|
||||||
|
'Galaxy S5 landscape',
|
||||||
|
'iPad',
|
||||||
|
'iPad landscape',
|
||||||
|
'iPad Mini',
|
||||||
|
'iPad Mini landscape',
|
||||||
|
'iPad Pro',
|
||||||
|
'iPad Pro landscape',
|
||||||
|
'iPhone 4',
|
||||||
|
'iPhone 4 landscape',
|
||||||
|
'iPhone 5',
|
||||||
|
'iPhone 5 landscape',
|
||||||
|
'iPhone 6',
|
||||||
|
'iPhone 6 landscape',
|
||||||
|
'iPhone 6 Plus',
|
||||||
|
'iPhone 6 Plus landscape',
|
||||||
|
'iPhone 7',
|
||||||
|
'iPhone 7 landscape',
|
||||||
|
'iPhone 7 Plus',
|
||||||
|
'iPhone 7 Plus landscape',
|
||||||
|
'iPhone 8',
|
||||||
|
'iPhone 8 landscape',
|
||||||
|
'iPhone 8 Plus',
|
||||||
|
'iPhone 8 Plus landscape',
|
||||||
|
'iPhone SE',
|
||||||
|
'iPhone SE landscape',
|
||||||
|
'iPhone X',
|
||||||
|
'iPhone X landscape',
|
||||||
|
'Kindle Fire HDX',
|
||||||
|
'Kindle Fire HDX landscape',
|
||||||
|
'LG Optimus L70',
|
||||||
|
'LG Optimus L70 landscape',
|
||||||
|
'Microsoft Lumia 550',
|
||||||
|
'Microsoft Lumia 950',
|
||||||
|
'Microsoft Lumia 950 landscape',
|
||||||
|
'Nexus 10',
|
||||||
|
'Nexus 10 landscape',
|
||||||
|
'Nexus 4',
|
||||||
|
'Nexus 4 landscape',
|
||||||
|
'Nexus 5',
|
||||||
|
'Nexus 5 landscape',
|
||||||
|
'Nexus 5X',
|
||||||
|
'Nexus 5X landscape',
|
||||||
|
'Nexus 6',
|
||||||
|
'Nexus 6 landscape',
|
||||||
|
'Nexus 6P',
|
||||||
|
'Nexus 6P landscape',
|
||||||
|
'Nexus 7',
|
||||||
|
'Nexus 7 landscape',
|
||||||
|
'Nokia Lumia 520',
|
||||||
|
'Nokia Lumia 520 landscape',
|
||||||
|
'Nokia N9',
|
||||||
|
'Nokia N9 landscape',
|
||||||
|
'Pixel 2',
|
||||||
|
'Pixel 2 landscape',
|
||||||
|
'Pixel 2 XL',
|
||||||
|
'Pixel 2 XL landscape'
|
||||||
|
]
|
13
screenshot/src/utils/widget/apidoc.js
Normal file
13
screenshot/src/utils/widget/apidoc.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @apiDefine needToken
|
||||||
|
* @apiHeader {String} Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiDefine SuccessExampleName
|
||||||
|
* @apiSuccessExample {json} Success-Response:
|
||||||
|
* HTTP/1.1 200 OK
|
||||||
|
* {
|
||||||
|
* "message": "ok"
|
||||||
|
* }
|
||||||
|
*/
|
67
screenshot/test/gif.js
Normal file
67
screenshot/test/gif.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-04-19 14:19:13
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-21 18:38:10
|
||||||
|
*/
|
||||||
|
// const fs2 = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const puppeteer = require('puppeteer')
|
||||||
|
|
||||||
|
const GIFEncoder = require('gif-encoder-2')
|
||||||
|
const { createWriteStream } = require('fs')
|
||||||
|
const PNG = require('png-js')
|
||||||
|
|
||||||
|
const params = { width: 1242/3, height: 2208/3 }
|
||||||
|
|
||||||
|
function decode(png) {
|
||||||
|
return new Promise((r) => {
|
||||||
|
png.decode((pixels) => r(pixels))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gifAddFrame(page, encoder) {
|
||||||
|
const pngBuffer = await page.screenshot({ clip: { width: params.width, height: params.height, x: 0, y: 0 } })
|
||||||
|
const png = new PNG(pngBuffer)
|
||||||
|
await decode(png).then((pixels) => encoder.addFrame(pixels))
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
slowMo: 0,
|
||||||
|
})
|
||||||
|
const page = await browser.newPage()
|
||||||
|
page.setViewport({ width: params.width, height: params.height })
|
||||||
|
await page.goto('http://localhost:3000/draw?tempid=519', {
|
||||||
|
waitUntil: ['networkidle0'],
|
||||||
|
timeout: 60000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dstPath = path.join(__dirname, `test.gif`)
|
||||||
|
// create a write stream for GIF data
|
||||||
|
const writeStream = createWriteStream(dstPath)
|
||||||
|
writeStream.on('close', () => {
|
||||||
|
console.log('create is done')
|
||||||
|
})
|
||||||
|
// createWriteStream().pipe(fs2.createWriteStream('test.gif'))
|
||||||
|
|
||||||
|
// setting gif encoder
|
||||||
|
// record gif
|
||||||
|
var encoder = new GIFEncoder(params.width, params.height)
|
||||||
|
encoder.createReadStream().pipe(writeStream)
|
||||||
|
encoder.start()
|
||||||
|
encoder.setRepeat(0)
|
||||||
|
encoder.setDelay(200)
|
||||||
|
// encoder.setQuality(10) // default
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await gifAddFrame(page, encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish encoder, test.gif saved
|
||||||
|
encoder.finish()
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
})()
|
16
screenshot/test/images.js
Normal file
16
screenshot/test/images.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-28 15:35:59
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-28 16:52:35
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = '/Users/mac/Documents/workSpace/Products/Management-Center/screenshot-service/static/screenshot-1-new.jpg'
|
||||||
|
|
||||||
|
const images = require('images')
|
||||||
|
|
||||||
|
const path233 = require('path')
|
||||||
|
// let tinyJpg = images(path233.resolve(__dirname, `../static/screenshot-1.png`)).size(300).encode('jpg', { quality: 20 })
|
||||||
|
// images(tinyJpg).save(path)
|
||||||
|
let tinyJpg = images(path233.resolve(__dirname, `../static/screenshot-1.png`)).size(300).save(path, { quality: 30 })
|
72
screenshot/tsconfig.json
Normal file
72
screenshot/tsconfig.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
|
// "lib": ["dom", "es2015"], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
// "declaration": true,// 是否生成声明文件
|
||||||
|
// "declarationDir": "./dist/types/",// 声明文件打包的位置 /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
"typeRoots": ["./node_modules/@types"], /* List of folders to include type definitions from. */
|
||||||
|
"types": ["node"], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
42
screenshot/webpack.config.js
Normal file
42
screenshot/webpack.config.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-12-27 10:15:07
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2021-12-27 11:22:29
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||||
|
const nodeExternals = require('webpack-node-externals');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: process.env.NODE_ENV,
|
||||||
|
target: 'node',
|
||||||
|
externals: [nodeExternals()],
|
||||||
|
entry: './src/main.ts',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'server.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
// 指定特定的ts编译配置,为了区分脚本的ts配置
|
||||||
|
configFile: path.resolve(__dirname, './tsconfig.json'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// plugins: [new BundleAnalyzerPlugin()],
|
||||||
|
}
|
4984
screenshot/yarn.lock
Normal file
4984
screenshot/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
30
src/App.vue
Normal file
30
src/App.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div id="appindex">
|
||||||
|
<div class="viewWrap">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#appindex {
|
||||||
|
min-width: 1180px;
|
||||||
|
.viewWrap {
|
||||||
|
background-color: #ffffff;
|
||||||
|
min-height: calc(110vh - 110px);
|
||||||
|
min-width: 1170px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fade-enter-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.fade-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
10
src/api/ai.ts
Normal file
10
src/api/ai.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-12 09:51:07
|
||||||
|
* @Description: AI接口
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-13 16:59:21
|
||||||
|
*/
|
||||||
|
import fetch from '@/utils/axios'
|
||||||
|
|
||||||
|
export const kt = (params: Type.Object = {}) => fetch('https://kt.palxp.com/api/remove', params, 'post', {}, { responseType: 'arraybuffer' })
|
39
src/api/album.ts
Normal file
39
src/api/album.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-26 12:47:40
|
||||||
|
* @Description: 相册 api 接口
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2021-08-30 10:45:49
|
||||||
|
*/
|
||||||
|
import fetch from '@/utils/axios'
|
||||||
|
import _config from '@/config'
|
||||||
|
const prefix = _config.API_URL + '/'
|
||||||
|
const API = {
|
||||||
|
init: prefix + 'pic/init',
|
||||||
|
getList: prefix + 'pic/list',
|
||||||
|
getToken: prefix + 'pic/getToken',
|
||||||
|
delOne: prefix + 'pic/delOne',
|
||||||
|
rename: prefix + 'pic/rename',
|
||||||
|
del: prefix + 'pic/del',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const init = (params: Type.Object = {}) => fetch(API.init, params, 'post')
|
||||||
|
|
||||||
|
export const getPicList = (params: Type.Object = {}) => fetch(API.getList, params)
|
||||||
|
|
||||||
|
export const getToken = (params: Type.Object = {}) => fetch(API.getToken, params)
|
||||||
|
|
||||||
|
export const deletePic = (params: Type.Object = {}) => fetch(API.delOne, params, 'post')
|
||||||
|
|
||||||
|
export const delPics = (params: Type.Object = {}) => fetch(API.del, params, 'post')
|
||||||
|
|
||||||
|
export const reName = (params: Type.Object = {}) => fetch(API.rename, params, 'post')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
init,
|
||||||
|
getPicList,
|
||||||
|
getToken,
|
||||||
|
deletePic,
|
||||||
|
delPics,
|
||||||
|
reName,
|
||||||
|
}
|
47
src/api/gaoding.ts
Normal file
47
src/api/gaoding.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-01-22 10:22:53
|
||||||
|
* @Description: 稿定接口
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-27 10:38:51
|
||||||
|
*/
|
||||||
|
import httpRequest from '@/utils/http-request'
|
||||||
|
|
||||||
|
export const CORS = 'https://juejin.palxp.com/cors?url='
|
||||||
|
export const GAODING_API_V2 = 'https://www.gaoding.com/api/v2'
|
||||||
|
export const GAODING_FONTER = 'https://fonter.dancf.com'
|
||||||
|
|
||||||
|
export const getFonts = async (pageNum: number): Promise<any> => {
|
||||||
|
return await httpRequest.get(`${CORS}${GAODING_API_V2}/fonts`, {
|
||||||
|
params: {
|
||||||
|
type: 'font',
|
||||||
|
page_size: 100,
|
||||||
|
page_num: pageNum,
|
||||||
|
region_id: 1,
|
||||||
|
biz_code: 1,
|
||||||
|
endpoint: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchFonts = async (name: string): Promise<any> => {
|
||||||
|
const url = `${CORS}https://www.gaoding.com/api/v2/font-fallbacks?font_name=${name}`
|
||||||
|
return await httpRequest.get(url, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSubsetFontParams {
|
||||||
|
font_id: number
|
||||||
|
content: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubsetFont(params: GetSubsetFontParams) {
|
||||||
|
return await httpRequest.get(`${GAODING_FONTER}/subset`, {
|
||||||
|
params: {
|
||||||
|
from_site: 'gaoding',
|
||||||
|
type: 'woff',
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
}
|
40
src/api/github.ts
Normal file
40
src/api/github.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-13 17:01:37
|
||||||
|
* @Description: github api
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-14 00:01:05
|
||||||
|
*/
|
||||||
|
import fetch from '@/utils/axios'
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
function getBase64(file: File) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
reader.onload = function (event: any) {
|
||||||
|
const fileContent = event.target.result
|
||||||
|
resolve(fileContent.split(',')[1])
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const putPic = async (file: any) => {
|
||||||
|
const content = typeof file === 'string' ? file : await getBase64(file)
|
||||||
|
const repo = 'shawnphang/files'
|
||||||
|
const d = new Date()
|
||||||
|
const path = `${d.getFullYear()}/${d.getMonth()}/${d.getTime()}${file.name?.split('.').pop() || '.png'}`
|
||||||
|
const imageUrl = 'https://api.github.com/repos/' + repo + '/contents/' + path
|
||||||
|
const body = {
|
||||||
|
branch: 'main',
|
||||||
|
message: 'upload',
|
||||||
|
content,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
await fetch(imageUrl, body, 'put', {
|
||||||
|
Authorization: 'token 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',
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
})
|
||||||
|
return `https://fastly.jsdelivr.net/gh/shawnphang/files@main/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { putPic }
|
37
src/api/home.ts
Normal file
37
src/api/home.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-19 18:43:22
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-13 18:19:14
|
||||||
|
*/
|
||||||
|
import fetch from '@/utils/axios'
|
||||||
|
import _config from '@/config'
|
||||||
|
|
||||||
|
// const screenshot_url = window.location.protocol + '//' + window.location.host + '/draw'
|
||||||
|
export const download = (params: Type.Object = {}) => `${_config.SCREEN_URL}/api/screenshots?id=${params.id}&width=${params.width}&height=${params.height}`
|
||||||
|
|
||||||
|
// 获取模板列表
|
||||||
|
export const getTempList = (params: Type.Object = {}) => fetch('design/list', params, 'get')
|
||||||
|
export const getTempDetail = (params: Type.Object = {}) => fetch('design/temp', params, 'get')
|
||||||
|
export const getCategories = (params: Type.Object = {}) => fetch('design/cate', params, 'get')
|
||||||
|
// 保存模板
|
||||||
|
export const saveTemp = (params: Type.Object = {}) => fetch('design/edit', params, 'post')
|
||||||
|
// export const delTemp = (params: Type.Object = {}) => fetch('/api/template/temp_del', params)
|
||||||
|
|
||||||
|
// 组件相关接口
|
||||||
|
export const getCompList = (params: Type.Object = {}) => fetch('design/list', params, 'get')
|
||||||
|
export const removeComp = (params: Type.Object = {}) => fetch('design/del', params, 'post')
|
||||||
|
// export const getCompDetail = (params: Type.Object = {}) => fetch('/api/template/temp_info', params, 'get')
|
||||||
|
|
||||||
|
// 保存作品
|
||||||
|
export const saveWorks = (params: Type.Object = {}) => fetch('design/save', params, 'post')
|
||||||
|
|
||||||
|
// 保存个人模板
|
||||||
|
export const saveMyTemp = (params: Type.Object = {}) => fetch('design/user/temp', params, 'post')
|
||||||
|
|
||||||
|
// 获取作品
|
||||||
|
export const getWorks = (params: Type.Object = {}) => fetch('design/poster', params, 'get')
|
||||||
|
|
||||||
|
// 作品列表
|
||||||
|
export const getMyDesign = (params: Type.Object = {}) => fetch('design/my', params, 'get')
|
16
src/api/index.ts
Normal file
16
src/api/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-19 18:43:22
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-01-22 18:22:13
|
||||||
|
*/
|
||||||
|
import * as home from './home'
|
||||||
|
import * as material from './material'
|
||||||
|
import * as gaoding from './gaoding'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
home,
|
||||||
|
material,
|
||||||
|
gaoding,
|
||||||
|
}
|
27
src/api/material.ts
Normal file
27
src/api/material.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-27 14:42:15
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 09:50:00
|
||||||
|
*/
|
||||||
|
import fetch from '@/utils/axios'
|
||||||
|
|
||||||
|
// 获取素材分类:
|
||||||
|
export const getKinds = (params: Type.Object = {}) => fetch('design/cate', params)
|
||||||
|
|
||||||
|
// 获取素材列表:
|
||||||
|
export const getList = (params: Type.Object = {}) => fetch('design/material', params)
|
||||||
|
|
||||||
|
// 获取字体
|
||||||
|
export const getFonts = (params: Type.Object = {}) => fetch('design/fonts', params)
|
||||||
|
|
||||||
|
// 图库列表
|
||||||
|
export const getImagesList = (params: Type.Object = {}) => fetch('design/imgs', params, 'get')
|
||||||
|
|
||||||
|
// 我的上传列表
|
||||||
|
export const getMyPhoto = (params: Type.Object = {}) => fetch('design/user/image', params)
|
||||||
|
export const deleteMyPhoto = (params: Type.Object = {}) => fetch('design/user/image/del', params, 'post')
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
export const addMyPhoto = (params: Type.Object = {}) => fetch('design/user/add_image', params)
|
21
src/assets/data/LayerIconList.ts
Normal file
21
src/assets/data/LayerIconList.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-04-15 10:51:50
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-15 10:51:51
|
||||||
|
*/
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
key: 'zIndex',
|
||||||
|
icon: 'icon-layer-up',
|
||||||
|
tip: '上一层',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zIndex',
|
||||||
|
icon: 'icon-layer-down',
|
||||||
|
tip: '下一层',
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
]
|
45
src/assets/data/QrCodeLocalization.ts
Normal file
45
src/assets/data/QrCodeLocalization.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-03-16 11:38:48
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-23 16:00:11
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
dotColorTypes: [
|
||||||
|
{
|
||||||
|
key: 'single',
|
||||||
|
value: '单色',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gradient',
|
||||||
|
value: '渐变色',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dotTypes: [
|
||||||
|
{
|
||||||
|
key: 'dots',
|
||||||
|
value: '圆点风格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rounded',
|
||||||
|
value: '圆润风格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'classy',
|
||||||
|
value: '经典风格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'classy-rounded',
|
||||||
|
value: '圆角风格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'square',
|
||||||
|
value: '方形风格',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'extra-rounded',
|
||||||
|
value: '特殊风格',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
114
src/assets/data/TextIconsData.ts
Normal file
114
src/assets/data/TextIconsData.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-02 18:27:27
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-25 10:30:38
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const styleIconList1 = [
|
||||||
|
{
|
||||||
|
key: 'fontWeight',
|
||||||
|
icon: 'icon-bold',
|
||||||
|
tip: '加粗',
|
||||||
|
value: ['normal', 'bold'],
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fontStyle',
|
||||||
|
icon: 'icon-italic',
|
||||||
|
tip: '斜体',
|
||||||
|
value: ['normal', 'italic'],
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'textDecoration',
|
||||||
|
icon: 'icon-underline',
|
||||||
|
tip: '下划线',
|
||||||
|
value: ['none', 'underline'],
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'textDecoration',
|
||||||
|
icon: 'icon-strikethrough',
|
||||||
|
tip: '删除线',
|
||||||
|
value: ['none', 'line-through'],
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'writingMode',
|
||||||
|
icon: 'icon-textorientation',
|
||||||
|
tip: '竖版文字',
|
||||||
|
value: ['horizontal-tb', 'vertical-rl'], // tb-rl
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
export const styleIconList2 = [
|
||||||
|
{
|
||||||
|
key: 'textAlign',
|
||||||
|
icon: 'icon-align-left-text',
|
||||||
|
tip: '左对齐',
|
||||||
|
value: 'left',
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'textAlign',
|
||||||
|
icon: 'icon-align-center-text',
|
||||||
|
tip: '居中对齐',
|
||||||
|
value: 'center',
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'textAlign',
|
||||||
|
icon: 'icon-align-right-text',
|
||||||
|
tip: '右对齐',
|
||||||
|
value: 'right',
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'textAlign',
|
||||||
|
icon: 'icon-align-justify-text',
|
||||||
|
tip: '两端对齐',
|
||||||
|
value: 'justify',
|
||||||
|
select: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const alignIconList = [
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-left',
|
||||||
|
tip: '左对齐',
|
||||||
|
value: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-center-horiz',
|
||||||
|
tip: '水平居中对齐',
|
||||||
|
value: 'ch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-right',
|
||||||
|
tip: '右对齐',
|
||||||
|
value: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-top',
|
||||||
|
tip: '上对齐',
|
||||||
|
value: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-center-verti',
|
||||||
|
tip: '垂直居中对齐',
|
||||||
|
value: 'cv',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-bottom',
|
||||||
|
tip: '下对齐',
|
||||||
|
value: 'bottom',
|
||||||
|
},
|
||||||
|
]
|
45
src/assets/data/alignListData.ts
Normal file
45
src/assets/data/alignListData.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-12 11:08:57
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-12 11:09:41
|
||||||
|
*/
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-left',
|
||||||
|
tip: '左对齐',
|
||||||
|
value: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-center-horiz',
|
||||||
|
tip: '水平居中对齐',
|
||||||
|
value: 'ch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-right',
|
||||||
|
tip: '右对齐',
|
||||||
|
value: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-top',
|
||||||
|
tip: '上对齐',
|
||||||
|
value: 'top',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-center-verti',
|
||||||
|
tip: '垂直居中对齐',
|
||||||
|
value: 'cv',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'align',
|
||||||
|
icon: 'icon-align-bottom',
|
||||||
|
tip: '下对齐',
|
||||||
|
value: 'bottom',
|
||||||
|
},
|
||||||
|
]
|
45
src/assets/data/widgetClassifyList.ts
Normal file
45
src/assets/data/widgetClassifyList.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-07-17 11:20:22
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 21:52:29
|
||||||
|
*/
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: '模板',
|
||||||
|
icon: 'icon-moban',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '素材',
|
||||||
|
icon: 'icon-sucai',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '文字',
|
||||||
|
icon: 'icon-wenzi',
|
||||||
|
show: false,
|
||||||
|
style: { fontWeight: 600 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '照片',
|
||||||
|
icon: 'icon-gallery',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '背景',
|
||||||
|
icon: 'icon-beijing',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '工具',
|
||||||
|
icon: 'icon-zujian01',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '我的',
|
||||||
|
icon: 'icon-shangchuan',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
]
|
BIN
src/assets/fonts/xpsj.subset.ttf
Normal file
BIN
src/assets/fonts/xpsj.subset.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/xpsj.subset.woff2
Normal file
BIN
src/assets/fonts/xpsj.subset.woff2
Normal file
Binary file not shown.
125
src/assets/styles/base.less
Normal file
125
src/assets/styles/base.less
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// element UI fix
|
||||||
|
.el-collapse-item__header {
|
||||||
|
padding: 0px 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.el-collapse-item__wrap {
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
.el-collapse {
|
||||||
|
border: 0px;
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
padding: 0;
|
||||||
|
.btn-prev {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
.btn-next {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
.el-pager > li {
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border: none;
|
||||||
|
color: #a5a5a5;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.el-pager {
|
||||||
|
.active {
|
||||||
|
background: #24b9ff;
|
||||||
|
border: none;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-icon-arrow-left {
|
||||||
|
color: #a5a5a5;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.el-icon-arrow-right {
|
||||||
|
color: #a5a5a5;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.el-select {
|
||||||
|
.el-input {
|
||||||
|
input {
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #262c33;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-select {
|
||||||
|
.el-tag {
|
||||||
|
line-height: 23px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-pagination__jump {
|
||||||
|
font-size: 14px !important;
|
||||||
|
.el-pagination__editor {
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #262c33;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
|
||||||
|
.clearfix {
|
||||||
|
&:after {
|
||||||
|
clear: both;
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.line-break {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击选择框样式
|
||||||
|
.layer {
|
||||||
|
&:hover {
|
||||||
|
outline: 2px dashed @main-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.layer-no-hover {
|
||||||
|
&:hover {
|
||||||
|
cursor: move;
|
||||||
|
outline: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .layer-active {
|
||||||
|
// outline: 1px dashed #000000 !important;
|
||||||
|
// &:hover {
|
||||||
|
// outline: 1px dashed #000000 !important;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
.layer-hover {
|
||||||
|
outline: 2px dashed @main-color !important;
|
||||||
|
&:hover {
|
||||||
|
outline: 2px dashed @main-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 锁定不可选中
|
||||||
|
.layer-lock {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
12
src/assets/styles/color.less
Normal file
12
src/assets/styles/color.less
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// @color-main: #1b1634;
|
||||||
|
@color-main: #ffffff;
|
||||||
|
@color-white: #ffffff;
|
||||||
|
@color-black: #000000;
|
||||||
|
@color-light-gray: #3e4651;
|
||||||
|
@color-dark-gray: #262c33;
|
||||||
|
@color-transparent: #ffffff00;
|
||||||
|
|
||||||
|
// @active-text-color: rgb(250, 131, 52);
|
||||||
|
// @main-color: rgb(250, 131, 52);
|
||||||
|
@active-text-color: #2254f4; // #1195db;
|
||||||
|
@main-color: #2254f4; // #1195db;
|
133
src/assets/styles/design.less
Normal file
133
src/assets/styles/design.less
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
@import './color.less';
|
||||||
|
// design index page
|
||||||
|
|
||||||
|
// Color variables (appears count calculates by raw css)
|
||||||
|
@color4: #50555b; // Appears 2 times
|
||||||
|
@color5: #808080; // Appears 2 times
|
||||||
|
|
||||||
|
// Width variables (appears count calculates by raw css)
|
||||||
|
@width0: 1180px; // Appears 3 times
|
||||||
|
@width1: 120px; // Appears 2 times
|
||||||
|
@width2: 100%; // Appears 8 times
|
||||||
|
|
||||||
|
@height2: 54px; // Appears 5 times
|
||||||
|
|
||||||
|
#page-design-index {
|
||||||
|
background-color: #4b678c;
|
||||||
|
// background-color: #4682b4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 500px;
|
||||||
|
min-width: @width0;
|
||||||
|
position: absolute;
|
||||||
|
width: @width2;
|
||||||
|
|
||||||
|
.top-nav {
|
||||||
|
height: @height2;
|
||||||
|
min-width: @width0;
|
||||||
|
position: relative;
|
||||||
|
width: @width2;
|
||||||
|
.top-nav-wrap {
|
||||||
|
border-bottom: 1px solid rgba(202, 119, 119, 0.1);
|
||||||
|
align-items: center;
|
||||||
|
background-color: @color-main;
|
||||||
|
display: flex;
|
||||||
|
height: @height2;
|
||||||
|
min-width: @width0;
|
||||||
|
position: fixed;
|
||||||
|
width: @width2;
|
||||||
|
.top-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: @color-black;
|
||||||
|
.name {
|
||||||
|
margin: 0 1rem;
|
||||||
|
// font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: TitleFont, PingFangSC-Semibold, PingFang SC;
|
||||||
|
}
|
||||||
|
.operation {
|
||||||
|
display: flex;
|
||||||
|
&-item {
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.disable {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #c2c2c2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .top-title {
|
||||||
|
// color: @color-black;
|
||||||
|
// cursor: pointer;
|
||||||
|
// flex: 1;
|
||||||
|
// padding-left: 3.2rem;
|
||||||
|
// // font-weight: bold;
|
||||||
|
// .input-wrap {
|
||||||
|
// width: 15rem;
|
||||||
|
// :deep(input) {
|
||||||
|
// border: none;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .top-icon-wrap {
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// padding-right: 20px;
|
||||||
|
// height: @height2;
|
||||||
|
// .top-icon {
|
||||||
|
// background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
// border-radius: 5px;
|
||||||
|
// color: @color-white;
|
||||||
|
// cursor: pointer;
|
||||||
|
// font-weight: bold;
|
||||||
|
// margin: 8px;
|
||||||
|
// padding: 5px 8px;
|
||||||
|
// &:hover {
|
||||||
|
// background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-design-index-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: @width2;
|
||||||
|
.page-design-wrap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .page-design-index-wrap ::-webkit-scrollbar {
|
||||||
|
// display: none; /* Chrome Safari */
|
||||||
|
// }
|
||||||
|
|
||||||
|
.extra-operation {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 52px;
|
||||||
|
}
|
||||||
|
.extra-operation::after {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
left: -50px;
|
||||||
|
background: #e8eaec;
|
||||||
|
height: 17px;
|
||||||
|
width: 1px;
|
||||||
|
margin: 7px 0 0 18px;
|
||||||
|
}
|
||||||
|
.shelter {
|
||||||
|
position: absolute;
|
||||||
|
box-shadow: 0 0 0 5000px rgba(255, 255, 255, 0.95);
|
||||||
|
z-index: 8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
3
src/assets/styles/index.less
Normal file
3
src/assets/styles/index.less
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@import './main.less';
|
||||||
|
@import './layout.less';
|
||||||
|
@import './base.less';
|
27
src/assets/styles/layout.less
Normal file
27
src/assets/styles/layout.less
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.editable-text {
|
||||||
|
outline: none;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
// display: -webkit-box !important;
|
||||||
|
// -webkit-box-orient: vertical !important;
|
||||||
|
// overflow: hidden !important;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box; //作为弹性伸缩盒子模型显示。
|
||||||
|
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-1 {
|
||||||
|
// -webkit-line-clamp: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
108
src/assets/styles/main.less
Normal file
108
src/assets/styles/main.less
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: TitleFont;
|
||||||
|
src: url(../fonts/xpsj.subset.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
--el-color-primary: #2254f4;
|
||||||
|
cursor: -webkit-image-set(
|
||||||
|
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABpNJREFUeF7tml9oW1Ucx391zs31zx770IEvG4M+jMyXvagLLCP4EIZtDCMjbBGEGEtLC4UxFToM1K50OLeQbjKnVNPG0r7UzEDukrU1WeKqLH3IS1Lmw4Z9cthU7XRz8r3cU8+uTe5NctbmYi5ckjb3npzf5/f9/TnnpoFKHw1EpD5xxxPuZH/zrxrD1s7HMK7YwQx/joi25fP5B42NjdtjsdiXJ06ceJeI/lYg4JW9BxhDgdACAOOfJ6Lty8vLD1pbW/GelpeXH0Wj0fF0Oj3k9/t/IqLHygkADIYhQJQCAONxvkBEO+Px+I9ms/klXi6FQuFJPB5PhsPhd65cuXKXiB4ZDYQWAHh8BxHt8vv9A16v15NOp+nChQvU09NDhw4dknkARCwWu3X9+nWP0UBoAdiuAGiyWq37I5FIDAYfPHiQVlZWZABGB6EFAArYSURNRNSSyWS+P3DgQLPH46FoNLoeDcVALCwsfOjz+b6r5dDQAwAh0AgAoVBo3OFwvDw9PU39/f3/qR5qELhgdnb2riRJ3loFoQVgm5IEAaC5r6/PPjIy8tG9e/fo8OHDRevnnj175NDo6OhYv6ZWQWiVQQBAHngRAKCCQqGQaWpqarDZbJTNZkt2NEYAoQWA9QHreUCSpOiRI0fafD4fXbt2TVdLVwxENpv91Ov1XtzKHKEFAJ9DBet5IBAIDHs8nmMoh06nUxcAdhEDcfToUWpuhqCIFhcXf00kEkNbBaIUAMyPAUAzhDBoMZvN++Px+Ld8OSyLAgZpaSG32y2fWw1CDwCEAfIAwgBu253L5Rb27t27Q10OjQhCDwBcw/oBVIPdoVDoq1LlUASIfD6/lkqlvna5XFh4sRabX3SxhVe5X/fU9XoBPFUOe3t73zx//vygVjksd2YsNDo7O6mtrU2+HQsvSZKCLpfLWwREuV9TFgA+D/DlcHehULijtxxWMkNAQC/Bg4hEIp+43e73FBDqJXglXyMnOa0D1wgph1pfpP68vb2dAoEAoXqwo6GhoYWI/lRBKHfof8fTcSfbGBFWDkt9J8IAHSQqBG/40tLSmiRJVz0ezwdEtEZEf3FLbx1mbHyJHgU8s3LIT8lisZDdbif0COxYXV3FfkN+YmJiKBgMJonoN+X8Q1EB243aFADCyyE8fOrUKdlo3tvJZPKX+fn5yOnTpwOKoQ+J6Hfu3BIFCCmHkDjzNttQUbL943A4nLp8+fLF27dv31ckDpkj3gEARrNzU3MACwG+K5RXh+WWw+7ubjm2AQEHJJ5KpX6empq6Ojo6KimJDUYzw2Eo/579jb5g06rAegJW1gUVlUOUNADAkclkVufm5ubHxsa+UHl7I+PxPxjMmiFswAoxnnlWbwKpuBzC47Ozs7LnBwcHJ86cOTOqeBZGwau8pxkEluWZt9Xb75vSCfJwKi6Hw8PDcmmD500m0+tKTCOumfG8tJnhzNMbtb9CjC9XARWVQ2R3eB+Hw+HwTk5OpokIZQwJjfc+DC7mbeYIYYbzca03BBiAssphMBiUd49v3Lhx32KxHONqOStlzOPFJC7caLWsywWguxzCcABAtrdarW8kk8k8kr8CASHAG8+eNz4zb29kqN5OkFeM7nI4MzND6OdDodCd48ePv0VEKwoANDWQP5/R8R3P1NsiAPB5oGQ5xGru3LlzWM4+ttvtHYlEIoeHSIr3mfyFlbNyZFxNCPB54KmHJurNUiQ+JEC/3/9NV1fX+4rxkD/zPhKeWvaV2lHxfeWGgLor3HCzFBumaHry+fzDffv2vaLInvc+/xC14smLuLESAJrlEM8N0fQMDAx8fPbs2c8477NVHGJ/y71fSR/AJ0N1OWzJ5XI/YLOUtbsmk+k1zvuQPjI/38eLcGJVY1SjAFYOYbD88DQUCgWxWYoZOZ3Ot8fHxxMKAMR+zSS+apOgOg/gmcEuQDh58uSrly5d+vzmzZs5m83WyTU9NZX4RADYqCsEBJwAgviG3BHzvPRZ7FclW5E3VxoCvArk3xApD04QDngPAGwjg+/4aiLxiVIAXw3YU2QYj+SIA8mOreXVHZ9IJ1Y1VjUK4FUg/5ROMZ6NiS6PX9Li+k1vdbXoVAuAh8D2C/DKDK35n8yJAKDuJ3gANel1kTlAS2E1/7koBdS8ocUmWAdgWNcJmnhdAYJAGnaYugIM6zpBE68rQBBIww5TV4BhXSdo4nUFCAJp2GHqCjCs6wRNvK4AQSANO0xdAYZ1naCJ/+8V8A+ZVYxuf/8kfgAAAABJRU5ErkJggg==)
|
||||||
|
2x
|
||||||
|
)
|
||||||
|
6 2,
|
||||||
|
default;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
color: #333333;
|
||||||
|
font-family: Hiragino Sans GB, Hiragino Sans GB W3, Arial, Microsoft Yahei, STHeiti, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
// font-size: 14px;
|
||||||
|
scrollbar-width: none; /* 火狐滚动条无法自定义宽度,只能通过此属性使滚动条宽度变细 */
|
||||||
|
-ms-overflow-style: none; /* 隐藏滚动条(在IE和Edge两个浏览器中很难更改样式,固采取隐藏方式) */
|
||||||
|
}
|
||||||
|
// html ::-webkit-scrollbar {
|
||||||
|
// display: none; /* Chrome Safari */
|
||||||
|
// }
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #ccd4de;
|
||||||
|
/*box-shadow: 0 0 1px hsl(0deg 0% 100% / 50%);*/
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
//background-color: #f0f1f3;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus input:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill,
|
||||||
|
textarea:-webkit-autofill:hover textarea:-webkit-autofill:focus,
|
||||||
|
select:-webkit-autofill,
|
||||||
|
select:-webkit-autofill:hover,
|
||||||
|
select:-webkit-autofill:focus {
|
||||||
|
//取消浏览器记住密码的样式
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
outline: 0;
|
||||||
|
&:-moz-focusring {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
outline: 0;
|
||||||
|
&:-moz-focusring {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag_active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.flutter {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.hide {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
118
src/common/hooks/dragHelper.ts
Normal file
118
src/common/hooks/dragHelper.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-10 14:58:48
|
||||||
|
* @Description: 拖拽优化
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-11 11:10:03
|
||||||
|
*/
|
||||||
|
import store from '@/store'
|
||||||
|
export default class dragHelper {
|
||||||
|
private cloneEl: any = null
|
||||||
|
private dragging: boolean = false
|
||||||
|
private initial: any = {}
|
||||||
|
private queue: any = []
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
if (this.dragging && this.cloneEl) {
|
||||||
|
const { offsetX, offsetY, width, height } = this.initial
|
||||||
|
// this.moveFlutter(e.pageX - offsetX, e.pageY - offsetY, this.distance(e))
|
||||||
|
this.moveFlutter(e.pageX - width / 2, e.pageY - height / 2, this.distance(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 鼠标抬起
|
||||||
|
window.addEventListener('mouseup', (e: any) => {
|
||||||
|
;(window as any).document.getElementById('app').classList.remove('drag_active')
|
||||||
|
const cl = e.target.classList
|
||||||
|
if (e.target?.id === 'page-design-canvas' || cl.contains('target') || cl.contains('drop__mask') || cl.contains('edit-text')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.finish(true)
|
||||||
|
}, 10)
|
||||||
|
} else this.finish()
|
||||||
|
})
|
||||||
|
// 鼠标离开了视窗
|
||||||
|
document.addEventListener('mouseleave', (e) => {
|
||||||
|
this.finish()
|
||||||
|
})
|
||||||
|
// 用户可能离开了浏览器
|
||||||
|
window.onblur = () => {
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 拖动开始 mousedown
|
||||||
|
*/
|
||||||
|
public start(e: any, finallySize: any) {
|
||||||
|
if (!this.cloneEl) {
|
||||||
|
store.commit('setDraging', true)
|
||||||
|
;(window as any).document.getElementById('app').classList.add('drag_active') // 整个鼠标全局变成抓取
|
||||||
|
// 选中了元素
|
||||||
|
this.cloneEl = e.target.cloneNode(true)
|
||||||
|
this.cloneEl.classList.add('flutter')
|
||||||
|
// 初始化数据
|
||||||
|
this.init(e, e.target, finallySize || e.target.offsetWidth, Math.random())
|
||||||
|
// 加载原图
|
||||||
|
// simulate(cloneEl.src, initial.flag)
|
||||||
|
this.cloneEl.style.width = e.target.offsetWidth
|
||||||
|
// e.target.parentElement.parentElement.appendChild(this.cloneEl)
|
||||||
|
;(window as any).document.getElementById('widget-panel').appendChild(this.cloneEl)
|
||||||
|
this.dragging = true
|
||||||
|
e.target.classList.add('hide') // 放在最后
|
||||||
|
this.queue.push(() => {
|
||||||
|
e.target.classList.remove('hide')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 开始拖动初始化
|
||||||
|
private init({ offsetX, offsetY, pageX, pageY, x, y }: any, { offsetWidth: width, offsetHeight: height }: any, finallySize: number, flag: any) {
|
||||||
|
this.initial = { offsetX, offsetY, pageX, pageY, width, height, finallySize, flag, x, y }
|
||||||
|
// store.commit('setDragInitData', { offsetX: 0, offsetY: 0 })
|
||||||
|
this.moveFlutter(pageX - offsetX, pageY - offsetY, 0, 0.3)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.moveFlutter(pageX - width / 2, pageY - height / 2, 0, 0.3)
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
// 改变漂浮元素(合并多个操作)
|
||||||
|
private moveFlutter(x: number, y: number, d = 0, lazy = 0) {
|
||||||
|
const { width, height, finallySize } = this.initial
|
||||||
|
let scale: any = null
|
||||||
|
if (width > finallySize) {
|
||||||
|
scale = d ? (width - d >= finallySize ? `transform: scale(${(width - d) / width});` : null) : null
|
||||||
|
} else scale = d ? (width + d <= finallySize ? `transform: scale(${(width + d) / width})` : null) : null
|
||||||
|
const options = [`left: ${x}px`, `top: ${y}px`, `width: ${width}px`, `height: ${height}px`]
|
||||||
|
scale && options.push(scale)
|
||||||
|
options.push(`transition: all ${lazy}s`)
|
||||||
|
this.changeStyle(options)
|
||||||
|
}
|
||||||
|
private changeStyle(arr: any) {
|
||||||
|
const original = this.cloneEl.style.cssText.split(';')
|
||||||
|
original.pop()
|
||||||
|
this.cloneEl.style.cssText = original.concat(arr).join(';') + ';'
|
||||||
|
}
|
||||||
|
// 结束/完成处理(动画)
|
||||||
|
private finish(done = false) {
|
||||||
|
this.dragging = false
|
||||||
|
store.commit('setDraging', false)
|
||||||
|
store.commit('selectItem', {})
|
||||||
|
if (!this.cloneEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!done) {
|
||||||
|
const { pageX, offsetX, pageY, offsetY } = this.initial
|
||||||
|
this.changeStyle([`left: ${pageX - offsetX}px`, `top: ${pageY - offsetY}px`, 'transform: scale(1)', 'transition: all 0.3s'])
|
||||||
|
}
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
this.queue.length && this.queue.shift()()
|
||||||
|
this.cloneEl && this.cloneEl.remove()
|
||||||
|
this.cloneEl = null
|
||||||
|
},
|
||||||
|
done ? 0 : 300,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 计算两点之间距离
|
||||||
|
private distance({ pageX, pageY }: any) {
|
||||||
|
const { pageX: x, pageY: y } = this.initial
|
||||||
|
return Math.hypot(pageX - x, pageY - y)
|
||||||
|
}
|
||||||
|
}
|
35
src/common/methods/DesignFeatures/setComponents.ts
Normal file
35
src/common/methods/DesignFeatures/setComponents.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-22 15:06:14
|
||||||
|
* @Description: 设置图片类型元素
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-07 14:57:51
|
||||||
|
*/
|
||||||
|
import store from '@/store'
|
||||||
|
export default async function setCompData(item: any) {
|
||||||
|
const group = typeof item === 'string' ? JSON.parse(item) : JSON.parse(JSON.stringify(item))
|
||||||
|
let parent: any = {}
|
||||||
|
Array.isArray(group) &&
|
||||||
|
group.forEach((element: any) => {
|
||||||
|
element.type === 'w-group' && (parent = element)
|
||||||
|
})
|
||||||
|
const { width: screenWidth, height: screenHeight } = store.getters.dPage
|
||||||
|
const { width: imgWidth, height: imgHeight } = parent
|
||||||
|
let ratio = 1
|
||||||
|
// 先限制在画布内,保证不超过边界
|
||||||
|
if (imgWidth > screenWidth || imgHeight > screenHeight) {
|
||||||
|
ratio = Math.min(screenWidth / imgWidth, screenHeight / imgHeight)
|
||||||
|
}
|
||||||
|
// 根据画布缩放比例再进行一次调整
|
||||||
|
if (ratio < 1) {
|
||||||
|
ratio *= store.getters.dZoom / 100
|
||||||
|
group.forEach((element: any) => {
|
||||||
|
element.fontSize && (element.fontSize *= ratio)
|
||||||
|
element.width *= ratio
|
||||||
|
element.height *= ratio
|
||||||
|
element.left *= ratio
|
||||||
|
element.top *= ratio
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return group
|
||||||
|
}
|
34
src/common/methods/DesignFeatures/setImage.ts
Normal file
34
src/common/methods/DesignFeatures/setImage.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-22 15:06:14
|
||||||
|
* @Description: 设置图片类型元素
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-10 17:37:27
|
||||||
|
*/
|
||||||
|
import store from '@/store'
|
||||||
|
import { getImage } from '../getImgDetail'
|
||||||
|
export default async function setItem2Data(item: any) {
|
||||||
|
const cloneItem = JSON.parse(JSON.stringify(item))
|
||||||
|
const { width: screenWidth, height: screenHeight } = store.getters.dPage
|
||||||
|
let { width: imgWidth, height: imgHeight } = item
|
||||||
|
if (!imgWidth || !imgHeight) {
|
||||||
|
const actual: any = await getImage(item.url)
|
||||||
|
cloneItem.width = imgWidth = actual.width
|
||||||
|
cloneItem.height = imgHeight = actual.height
|
||||||
|
}
|
||||||
|
let ratio = 1
|
||||||
|
// 先限制在画布内,保证不超过边界
|
||||||
|
if (imgWidth > screenWidth || imgHeight > screenHeight) {
|
||||||
|
ratio = Math.min(screenWidth / imgWidth, screenHeight / imgHeight)
|
||||||
|
}
|
||||||
|
// 根据画布缩放比例再进行一次调整
|
||||||
|
if (ratio < 1) {
|
||||||
|
cloneItem.width = cloneItem.width * ratio * (store.getters.dZoom / 100)
|
||||||
|
cloneItem.height = cloneItem.height * ratio * (store.getters.dZoom / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloneItem.canvasWidth = cloneItem.width * (store.getters.dZoom / 100)
|
||||||
|
// cloneItem.canvasHeight = cloneItem.height * (store.getters.dZoom / 100)
|
||||||
|
|
||||||
|
return cloneItem
|
||||||
|
}
|
47
src/common/methods/DesignFeatures/setWidgetData.ts
Normal file
47
src/common/methods/DesignFeatures/setWidgetData.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-22 15:06:14
|
||||||
|
* @Description: 设置元素时根据类型处理
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-03 10:10:53
|
||||||
|
*/
|
||||||
|
// import store from '@/store'
|
||||||
|
// import { getImage } from '../getImgDetail'
|
||||||
|
import setImageData from '@/common/methods/DesignFeatures/setImage'
|
||||||
|
import wText from '@/components/modules/widgets/wText/wText.vue'
|
||||||
|
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
|
||||||
|
import wSvg from '@/components/modules/widgets/wSvg/wSvg.vue'
|
||||||
|
export default async function(type: string, item: any, data: any) {
|
||||||
|
let setting = data
|
||||||
|
if (type === 'text') {
|
||||||
|
!item.fontFamily && !item.color ? (setting = JSON.parse(JSON.stringify(wText.setting))) : (setting = item)
|
||||||
|
!setting.text ? (setting.text = '双击编辑文字') : (setting.text = decodeURIComponent(setting.text)) // item.text
|
||||||
|
setting.fontSize = item.fontSize
|
||||||
|
setting.width = item.width || item.fontSize * setting.text.length
|
||||||
|
setting.fontWeight = item.fontWeight
|
||||||
|
}
|
||||||
|
if (type === 'image' || type === 'mask') {
|
||||||
|
setting = JSON.parse(JSON.stringify(wImage.setting))
|
||||||
|
const img = await setImageData(item.value)
|
||||||
|
setting.width = img.width
|
||||||
|
setting.height = img.height // parseInt(100 / item.value.ratio, 10)
|
||||||
|
setting.imgUrl = item.value.url
|
||||||
|
}
|
||||||
|
if (type === 'mask') {
|
||||||
|
setting.mask = item.value.url
|
||||||
|
}
|
||||||
|
if (type === 'svg') {
|
||||||
|
setting = JSON.parse(JSON.stringify(wSvg.setting))
|
||||||
|
const img = await setImageData(item.value)
|
||||||
|
setting.width = img.width
|
||||||
|
setting.height = img.height // parseInt(100 / item.value.ratio, 10)
|
||||||
|
setting.svgUrl = item.value.url
|
||||||
|
const models = JSON.parse(item.value.model)
|
||||||
|
for (const key in models) {
|
||||||
|
if (Object.hasOwnProperty.call(models, key)) {
|
||||||
|
setting[key] = models[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return setting
|
||||||
|
}
|
71
src/common/methods/QiNiu.ts
Normal file
71
src/common/methods/QiNiu.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-29 20:35:31
|
||||||
|
* @Description: 七牛上传方法
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-11 22:02:12
|
||||||
|
*/
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import api from '@/api/album'
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
bucket: string
|
||||||
|
prePath?: string
|
||||||
|
fullPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
upload: async (file: File, options: Options, cb?: Function) => {
|
||||||
|
const win: any = window
|
||||||
|
let name = ''
|
||||||
|
const suffix = file.type.split('/')[1] // 文件后缀
|
||||||
|
if (!options.fullPath) {
|
||||||
|
// const DT: any = await exifGetTime(file) // 照片时间
|
||||||
|
const DT: any = new Date()
|
||||||
|
const YM = `${dayjs(DT).format('YYYY')}/${dayjs(DT).format('MM')}/` // 文件时间分类
|
||||||
|
const keyName = YM + new Date(DT).getTime()
|
||||||
|
const prePath = options.prePath ? options.prePath + '/' : ''
|
||||||
|
name = `${prePath}${keyName}` + `.${suffix}` // 文件名
|
||||||
|
} else name = options.fullPath + `.${suffix}` // 文件名
|
||||||
|
const token = await api.getToken({ bucket: options.bucket, name })
|
||||||
|
const exOption = {
|
||||||
|
useCdnDomain: true, // 使用cdn加速
|
||||||
|
}
|
||||||
|
const observable = win.qiniu.upload(file, name, token, {}, exOption)
|
||||||
|
return new Promise((resolve: Function, reject: Function) => {
|
||||||
|
observable.subscribe({
|
||||||
|
next: (result: any) => {
|
||||||
|
cb && cb(result) // result.total.percent -> 展示进度
|
||||||
|
},
|
||||||
|
error: (e: any) => {
|
||||||
|
reject(e)
|
||||||
|
},
|
||||||
|
complete: (result: any) => {
|
||||||
|
resolve(result)
|
||||||
|
// cb && cb(result) // result.total.percent -> 展示进度
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// function exifGetTime(img: any) {
|
||||||
|
// const win = window as any
|
||||||
|
// return new Promise((resolve) => {
|
||||||
|
// const file = img.originFileObj || img
|
||||||
|
// win.EXIF.getData(file, function() {
|
||||||
|
// const DT = win.EXIF.getAllTags(this).DateTimeOriginal || win.EXIF.getAllTags(this).DateTime
|
||||||
|
// if (DT) {
|
||||||
|
// if (DT.split(' ').length > 1) {
|
||||||
|
// const date = DT.split(' ')[0].replace(/:/g, '/')
|
||||||
|
// const time = DT.split(' ')[1]
|
||||||
|
// resolve(dayjs(`${date} ${time}`).isValid() ? `${date} ${time}` : date)
|
||||||
|
// } else {
|
||||||
|
// resolve(DT)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// resolve(new Date())
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
36
src/common/methods/addMouseWheel.ts
Normal file
36
src/common/methods/addMouseWheel.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-03-25 13:43:07
|
||||||
|
* @Description: 添加滚动监听
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-25 14:32:19
|
||||||
|
*/
|
||||||
|
import store from '@/store'
|
||||||
|
export default function(el: Element | string, cb: Function, altLimit: boolean = true) {
|
||||||
|
const box = typeof el === 'string' ? document.getElementById(el) : el
|
||||||
|
addEvent(box, 'mousewheel', (e: any) => {
|
||||||
|
const ev = e || window.event
|
||||||
|
const down = ev.wheelDelta ? ev.wheelDelta < 0 : ev.detail > 0
|
||||||
|
// if (down) {
|
||||||
|
// console.log('鼠标滚轮向下---------')
|
||||||
|
// } else {
|
||||||
|
// console.log('鼠标滚轮向上++++++++++')
|
||||||
|
// }
|
||||||
|
if (altLimit && store.getters.dAltDown) {
|
||||||
|
ev.preventDefault()
|
||||||
|
cb(down)
|
||||||
|
} else if (!altLimit) {
|
||||||
|
ev.preventDefault()
|
||||||
|
cb(down)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEvent(obj: any, xEvent: string, fn: Function) {
|
||||||
|
if (obj.attachEvent) {
|
||||||
|
obj.attachEvent('on' + xEvent, fn)
|
||||||
|
} else {
|
||||||
|
obj.addEventListener(xEvent, fn, false)
|
||||||
|
}
|
||||||
|
}
|
23
src/common/methods/confirm.ts
Normal file
23
src/common/methods/confirm.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-02-03 16:30:18
|
||||||
|
* @Description: Type: success / info / warning / error
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-03 16:43:01
|
||||||
|
*/
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
export default (title: string = '提示', message: string = '', type: any = 'success') => {
|
||||||
|
return new Promise((resolve: Function) => {
|
||||||
|
ElMessageBox.confirm(message, title, {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
79
src/common/methods/download/download.ts
Normal file
79
src/common/methods/download/download.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-30 15:52:59
|
||||||
|
* @Description: 下载远程图片
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 16:54:51
|
||||||
|
*/
|
||||||
|
export default (src: string, cb: Function) => {
|
||||||
|
return new Promise((resolve: any) => {
|
||||||
|
// const image = new Image()
|
||||||
|
// // 解决跨域 Canvas 污染问题
|
||||||
|
// image.setAttribute('crossOrigin', 'anonymous')
|
||||||
|
// image.onload = function() {
|
||||||
|
// const canvas = document.createElement('canvas')
|
||||||
|
// canvas.width = image.width
|
||||||
|
// canvas.height = image.height
|
||||||
|
|
||||||
|
// const context = canvas.getContext('2d')
|
||||||
|
// context?.drawImage(image, 0, 0, image.width, image.height)
|
||||||
|
// const url = canvas.toDataURL('image/jpg')
|
||||||
|
|
||||||
|
// const a = document.createElement('a')
|
||||||
|
// const event = new MouseEvent('click')
|
||||||
|
|
||||||
|
// a.download = String(new Date().getTime())
|
||||||
|
// a.href = url
|
||||||
|
|
||||||
|
// // 触发a的单击事件
|
||||||
|
// a.dispatchEvent(event)
|
||||||
|
// resolve()
|
||||||
|
// }
|
||||||
|
|
||||||
|
fetchImageDataFromUrl(src, (progress: number, xhr: XMLHttpRequest) => {
|
||||||
|
cb(progress, xhr)
|
||||||
|
}).then((res: any) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const txt: any = event?.target?.result
|
||||||
|
// image.src = txt
|
||||||
|
const a = document.createElement('a')
|
||||||
|
const mE = new MouseEvent('click')
|
||||||
|
// TODO: 部分浏览器会丢失后缀,所以补上
|
||||||
|
a.download = String(new Date().getTime()) + '.png'
|
||||||
|
a.href = txt
|
||||||
|
// 触发a的单击事件
|
||||||
|
a.dispatchEvent(mE)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
if (!res) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchImageDataFromUrl(url: string, cb: Function) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
let totalLength: any = ''
|
||||||
|
xhr.open('GET', url)
|
||||||
|
xhr.responseType = 'blob'
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
totalLength = Number(xhr.getResponseHeader('content-length')) // 'cache-control'
|
||||||
|
}
|
||||||
|
xhr.onprogress = function (event) {
|
||||||
|
cb((event.loaded / totalLength) * 100, xhr)
|
||||||
|
}
|
||||||
|
xhr.onload = function () {
|
||||||
|
if (xhr.status < 400) resolve(this.response)
|
||||||
|
else resolve(null)
|
||||||
|
}
|
||||||
|
xhr.onerror = function (e) {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
xhr.send()
|
||||||
|
})
|
||||||
|
}
|
13
src/common/methods/download/downloadBase64File.ts
Normal file
13
src/common/methods/download/downloadBase64File.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-12 19:37:14
|
||||||
|
* @Description: 下载 base64
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 19:37:39
|
||||||
|
*/
|
||||||
|
export default (base64Data: string, fileName: string) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = base64Data
|
||||||
|
link.download = fileName
|
||||||
|
link.click()
|
||||||
|
}
|
15
src/common/methods/download/downloadBlob.ts
Normal file
15
src/common/methods/download/downloadBlob.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-12 19:36:16
|
||||||
|
* @Description: 下载 blob
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 19:36:41
|
||||||
|
*/
|
||||||
|
export default (blob: Blob, fileName: string) => {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = fileName
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
14
src/common/methods/download/index.ts
Normal file
14
src/common/methods/download/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-12 16:54:12
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 19:38:09
|
||||||
|
*/
|
||||||
|
import downloadImg from './download'
|
||||||
|
import downloadBase64File from './downloadBase64File'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
downloadImg,
|
||||||
|
downloadBase64File,
|
||||||
|
}
|
110
src/common/methods/fonts/index.ts
Normal file
110
src/common/methods/fonts/index.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-01-08 09:43:37
|
||||||
|
* @Description: 字体处理
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-30 14:13:26
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import { getFonts } from '@/api/gaoding'
|
||||||
|
// import { isSupportFontFamily, blob2Base64 } from './utils'
|
||||||
|
import { getFonts } from '@/api/material'
|
||||||
|
// import store from '@/store'
|
||||||
|
|
||||||
|
const fontList: any = []
|
||||||
|
// const download: any = {}
|
||||||
|
export const useFontStore = {
|
||||||
|
list: fontList,
|
||||||
|
// download,
|
||||||
|
async init() {
|
||||||
|
this.list = []
|
||||||
|
const localFonts: any = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
|
||||||
|
if (localFonts.length > 0) {
|
||||||
|
this.list.push(...localFonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.list.length === 0) {
|
||||||
|
const res = await getFonts({ pageSize: 400 })
|
||||||
|
this.list.unshift(
|
||||||
|
...res.list.map((x: any) => {
|
||||||
|
const { alias, oid, value, preview, woff, lang } = x
|
||||||
|
return { id: oid, value, preview, alias, url: woff, lang }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
localStorage.setItem('FONTS', JSON.stringify(this.list))
|
||||||
|
}
|
||||||
|
// store.dispatch('setFonts', this.list)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const useFontStore = () => {
|
||||||
|
// return {
|
||||||
|
// list: fontList,
|
||||||
|
// download,
|
||||||
|
// async init() {
|
||||||
|
// this.list = []
|
||||||
|
// const localFonts: any = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
|
||||||
|
// if (localFonts.length > 0) {
|
||||||
|
// this.list.push(...localFonts)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.list.length === 0) {
|
||||||
|
// const res = await getFonts({ pageSize: 400 })
|
||||||
|
// this.list.unshift(
|
||||||
|
// ...res.map((x: any) => {
|
||||||
|
// const { content, id, name, preview } = x
|
||||||
|
// return { id, name, preview: preview.url, alias: content.alias, family: content.family, lang: content.lang, ttf: content.ttf, url: content.woff }
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// localStorage.setItem('FONTS', JSON.stringify(this.list))
|
||||||
|
// }
|
||||||
|
// console.log(this.list)
|
||||||
|
// },
|
||||||
|
// getList() {
|
||||||
|
// return fontList
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const useFontStore = () => {
|
||||||
|
// return {
|
||||||
|
// list: fontList,
|
||||||
|
// download,
|
||||||
|
// async init() {
|
||||||
|
// this.list = []
|
||||||
|
// const localFonts: any = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
|
||||||
|
// if (localFonts.length > 0) {
|
||||||
|
// this.list.push(...localFonts)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.list.length === 0) {
|
||||||
|
// for (let i = 1; i < 99; i += 1) {
|
||||||
|
// const res = await getFonts(i)
|
||||||
|
// this.list.unshift(
|
||||||
|
// ...res.map((x: any) => {
|
||||||
|
// const { content, id, name, preview } = x
|
||||||
|
// return { id, name, preview: preview.url, alias: content.alias, family: content.family, lang: content.lang, ttf: content.ttf, url: content.woff }
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// if (res.length < 100) break
|
||||||
|
// }
|
||||||
|
// localStorage.setItem('FONTS', JSON.stringify(this.list))
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// async addFont2Style(name: string, url: string) {
|
||||||
|
// // if (this.download[name]) return;
|
||||||
|
// if (isSupportFontFamily(name)) return
|
||||||
|
|
||||||
|
// const response = await fetch(url, { headers: { responseType: 'blob' } })
|
||||||
|
// const blob = await response.blob()
|
||||||
|
// const ff = new FontFace(name, `url(${URL.createObjectURL(blob)})`)
|
||||||
|
// const f = await ff.load()
|
||||||
|
// ;(document.fonts as FontFaceSet).add(f)
|
||||||
|
|
||||||
|
// const b64 = await blob2Base64(blob)
|
||||||
|
// // 使用 base64 是为了方便将 DOM 生成图片
|
||||||
|
// this.download[name] = b64
|
||||||
|
// // document.head.appendChild(generateFontStyle(name, b64));
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
124
src/common/methods/fonts/utils.ts
Normal file
124
src/common/methods/fonts/utils.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
interface TextItem {
|
||||||
|
text: string
|
||||||
|
[porpname: string]: any
|
||||||
|
}
|
||||||
|
interface CloudText {
|
||||||
|
fontFamily: string
|
||||||
|
fontSize: number
|
||||||
|
textAlign: string
|
||||||
|
color: string
|
||||||
|
textDecoration: string
|
||||||
|
writingMode: string
|
||||||
|
fontWeight: string | number
|
||||||
|
fontStyle: string
|
||||||
|
lineHeight: number
|
||||||
|
shadows: Record<string, unknown>[]
|
||||||
|
strokes: Record<string, unknown>[]
|
||||||
|
text: string
|
||||||
|
texts: TextItem[]
|
||||||
|
type: string
|
||||||
|
letterSpacing: number
|
||||||
|
[propsName: string]: unknown
|
||||||
|
}
|
||||||
|
const CLOUD_TYPE = {
|
||||||
|
text: 'text',
|
||||||
|
image: 'image',
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.zhangxinxu.com/wordpress/2018/02/js-detect-suppot-font-family/
|
||||||
|
export const isSupportFontFamily = (f: string) => {
|
||||||
|
if (typeof f != 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const h = 'Arial'
|
||||||
|
if (f.toLowerCase() == h.toLowerCase()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const e = 'a'
|
||||||
|
const d = 100
|
||||||
|
const a = 100
|
||||||
|
const i = 100
|
||||||
|
const c = document.createElement('canvas')
|
||||||
|
const b = c.getContext('2d') as CanvasRenderingContext2D
|
||||||
|
|
||||||
|
c.width = a
|
||||||
|
c.height = i
|
||||||
|
|
||||||
|
b.textAlign = 'center'
|
||||||
|
b.fillStyle = 'black'
|
||||||
|
b.textBaseline = 'middle'
|
||||||
|
|
||||||
|
const g = (j: string) => {
|
||||||
|
b.clearRect(0, 0, a, i)
|
||||||
|
b.font = d + 'px ' + j + ', ' + h
|
||||||
|
b.fillText(e, a / 2, i / 2)
|
||||||
|
const k = b.getImageData(0, 0, a, i).data
|
||||||
|
return [].slice.call(k).filter((l) => {
|
||||||
|
return l != 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return g(h).join('') !== g(f).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成字体 style
|
||||||
|
export function generateFontStyle(name: string, url: string): HTMLStyleElement {
|
||||||
|
const el = document.createElement('style')
|
||||||
|
el.id = name
|
||||||
|
// el.classList.add('font-face');
|
||||||
|
el.innerHTML = `@font-face { font-family: "${name}"; src: local("${name}"), url("${url}"); }`
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到使用到的所有字体
|
||||||
|
export function filterSkyFonts() {
|
||||||
|
const fonts: string[] = []
|
||||||
|
// const textClouds = sky.state.clouds.filter(
|
||||||
|
// (cloud) => cloud.type === CLOUD_TYPE.text,
|
||||||
|
// );
|
||||||
|
const textClouds: any = []
|
||||||
|
|
||||||
|
;((textClouds as unknown) as CloudText[]).forEach((cloud) => {
|
||||||
|
// 找到文字组件字体
|
||||||
|
if (cloud.fontFamily && !fonts.includes(cloud.fontFamily)) {
|
||||||
|
fonts.push(cloud.fontFamily)
|
||||||
|
}
|
||||||
|
// 找到文字组件子级字体
|
||||||
|
cloud.texts.forEach((text) => {
|
||||||
|
if (text.fontFamily && !fonts.includes(text.fontFamily)) {
|
||||||
|
fonts.push(text.fontFamily)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return fonts
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base642Blob(b64Data: string, contentType = '', sliceSize = 512) {
|
||||||
|
const byteCharacters = atob(b64Data)
|
||||||
|
const byteArrays = []
|
||||||
|
|
||||||
|
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||||
|
const slice = byteCharacters.slice(offset, offset + sliceSize)
|
||||||
|
|
||||||
|
const byteNumbers = new Array(slice.length)
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
byteArrays.push(byteArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob(byteArrays, { type: contentType })
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function blob2Base64(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
fileReader.onload = () => {
|
||||||
|
resolve(fileReader.result as string)
|
||||||
|
}
|
||||||
|
fileReader.onerror = reject
|
||||||
|
fileReader.readAsDataURL(blob)
|
||||||
|
})
|
||||||
|
}
|
27
src/common/methods/getImgDetail.ts
Normal file
27
src/common/methods/getImgDetail.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-23 17:25:35
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-24 00:16:59
|
||||||
|
*/
|
||||||
|
export const getImage = (imgItem: string | File) => {
|
||||||
|
// 创建对象
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
// 改变图片的src
|
||||||
|
const url = window.URL || window.webkitURL
|
||||||
|
img.src = typeof imgItem === 'string' ? imgItem : url.createObjectURL(imgItem)
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 判断是否有缓存
|
||||||
|
if (img.complete) {
|
||||||
|
resolve(img)
|
||||||
|
} else {
|
||||||
|
// 加载完成执行
|
||||||
|
img.onload = function() {
|
||||||
|
resolve(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
35
src/common/methods/handleTransform.ts
Normal file
35
src/common/methods/handleTransform.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-01-31 10:45:53
|
||||||
|
* @Description: 用于修改transform字符串
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-02-18 16:54:13
|
||||||
|
*/
|
||||||
|
export function getTransformAttribute(target: any, attr: string = '') {
|
||||||
|
const tf = target.style.transform
|
||||||
|
const iof = tf.indexOf(attr)
|
||||||
|
const half = tf.substring(iof + attr.length + 1)
|
||||||
|
return half.substring(0, half.indexOf(')'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTransformAttribute(target: any, attr: string, value: string | number = 0) {
|
||||||
|
const tf = target?.style.transform
|
||||||
|
if (!tf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const iof = tf.indexOf(attr)
|
||||||
|
const FRONT = tf.slice(0, iof + attr.length + 1)
|
||||||
|
const half = tf.substring(iof + attr.length + 1)
|
||||||
|
const END = half.substring(half.indexOf(')'))
|
||||||
|
target.style.transform = FRONT + value + END
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMatrix(params: any) {
|
||||||
|
const result = []
|
||||||
|
for (const key in params) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
||||||
|
result.push(params[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
39
src/common/methods/helper/index.ts
Normal file
39
src/common/methods/helper/index.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-04-15 11:16:20
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-15 11:22:49
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 显示全局提示
|
||||||
|
* @param content
|
||||||
|
* @param tooltipVisible
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function toolTip(content: string) {
|
||||||
|
const tooltip = drawTooltip(content)
|
||||||
|
document.body.appendChild(tooltip)
|
||||||
|
setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTooltip(content: string, tooltipVisible = true) {
|
||||||
|
const tooltip: any = document.createElement('div')
|
||||||
|
tooltip.id = 'color-pipette-tooltip-container'
|
||||||
|
tooltip.innerHTML = content
|
||||||
|
tooltip.style = `
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 9%;
|
||||||
|
z-index: 10002;
|
||||||
|
display: ${tooltipVisible ? 'flex' : 'none'};
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 18px;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
return tooltip
|
||||||
|
}
|
18
src/common/methods/loading.ts
Normal file
18
src/common/methods/loading.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-30 16:28:40
|
||||||
|
* @Description: 加载遮罩 / 弹窗
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-01-20 17:46:20
|
||||||
|
*/
|
||||||
|
import { ElLoading } from 'element-plus'
|
||||||
|
export default (text: string = 'loading') => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text,
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
})
|
||||||
|
return loading
|
||||||
|
// loading.close()
|
||||||
|
}
|
15
src/common/methods/notification.ts
Normal file
15
src/common/methods/notification.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-30 16:28:40
|
||||||
|
* @Description: 加载遮罩 / 弹窗
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-01-20 18:19:20
|
||||||
|
*/
|
||||||
|
import { ElNotification } from 'element-plus'
|
||||||
|
export default (title: string, message: string = '', type: any = 'success') => {
|
||||||
|
ElNotification({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
}
|
64
src/common/methods/target.ts
Normal file
64
src/common/methods/target.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-10 15:42:12
|
||||||
|
* @Description: 处理与目标组件相关
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-13 16:17:54
|
||||||
|
*/
|
||||||
|
// TODO: Group类型比较特殊,所以需要全量循环并判断是否为group
|
||||||
|
const arr = ['w-text', 'w-image', 'w-svg', 'w-group', 'w-qrcode']
|
||||||
|
|
||||||
|
export function getTarget(currentTarget: any) {
|
||||||
|
let collector: any[] = []
|
||||||
|
let groupTarger: any = null
|
||||||
|
let saveTarger: any = null
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
function findTarget(target: any) {
|
||||||
|
if (!target || target.id === 'page-design') {
|
||||||
|
if (collector.length > 1) {
|
||||||
|
resolve(groupTarger)
|
||||||
|
} else {
|
||||||
|
resolve(saveTarger || currentTarget)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = Array.from(target.classList)
|
||||||
|
|
||||||
|
collector = collector.concat(
|
||||||
|
t.filter((x) => {
|
||||||
|
arr.includes(x) && (saveTarger = target)
|
||||||
|
x === 'w-group' && (groupTarger = target)
|
||||||
|
return arr.includes(x)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
findTarget(target.parentElement)
|
||||||
|
}
|
||||||
|
findTarget(currentTarget)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFinalTarget(currentTarget: any) {
|
||||||
|
let collector: any[] = []
|
||||||
|
let groupTarger: any = null
|
||||||
|
let saveTarger: any = null
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
function findTarget(target: any) {
|
||||||
|
if (!target || target.id === 'page-design') {
|
||||||
|
resolve(target)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = Array.from(target.classList)
|
||||||
|
|
||||||
|
collector = collector.concat(
|
||||||
|
t.filter((x) => {
|
||||||
|
arr.includes(x) && (saveTarger = target)
|
||||||
|
x === 'w-group' && (groupTarger = target)
|
||||||
|
return arr.includes(x)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
findTarget(target.parentElement)
|
||||||
|
}
|
||||||
|
findTarget(currentTarget)
|
||||||
|
})
|
||||||
|
}
|
106
src/components/business/cropper/CropImage.vue
Normal file
106
src/components/business/cropper/CropImage.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-09-28 20:06:25
|
||||||
|
* @Description: 裁剪组件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-29 17:58:00
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="裁剪图片" width="80%" :before-close="handleClose" @close="cancel">
|
||||||
|
<div id="wrap" v-loading="loading" style="height: 50vh">
|
||||||
|
<img v-show="url" ref="imgBox" style="visibility: hidden" :src="url" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="cancel">取消</el-button>
|
||||||
|
<el-button :loading="loading" plain type="primary" @click="ok">确认</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import { ElDialog } from 'element-plus'
|
||||||
|
import { ref, defineComponent, toRefs, reactive, nextTick } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { ElDialog },
|
||||||
|
emits: ['done'],
|
||||||
|
setup(props, context) {
|
||||||
|
const store = useStore()
|
||||||
|
const state = reactive({
|
||||||
|
loading: false,
|
||||||
|
url: '',
|
||||||
|
})
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const imgBox = ref<HTMLImageElement | any>()
|
||||||
|
let cropData: any = null
|
||||||
|
let cropper: any = null
|
||||||
|
|
||||||
|
const handleClose = (done: any) => {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
const open = async (item: any, data = {}) => {
|
||||||
|
state.loading = true
|
||||||
|
item.rawImg = item.rawImg ? item.rawImg : item.imgUrl
|
||||||
|
cropData = data
|
||||||
|
state.url = item.rawImg
|
||||||
|
store.commit('setShowMoveable', false)
|
||||||
|
dialogVisible.value = true
|
||||||
|
await nextTick()
|
||||||
|
setEdit()
|
||||||
|
}
|
||||||
|
const setEdit = () => {
|
||||||
|
cropper = new Cropper(imgBox.value, {
|
||||||
|
// aspectRatio: imgBox.value.width / imgBox.value.height,
|
||||||
|
dragMode: 'move',
|
||||||
|
viewMode: 1,
|
||||||
|
cropBoxMovable: false,
|
||||||
|
// cropBoxResizable: false,
|
||||||
|
highlight: false,
|
||||||
|
background: true,
|
||||||
|
// crop(event) {
|
||||||
|
// console.log(event);
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
imgBox.value.addEventListener('ready', function() {
|
||||||
|
state.loading = false
|
||||||
|
if (this.cropper === cropper) {
|
||||||
|
cropData && cropper.setData(cropData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const ok = () => {
|
||||||
|
state.loading = true
|
||||||
|
setTimeout(async () => {
|
||||||
|
const newImg = cropper.getCroppedCanvas({ maxWidth: 4096, minWidth: 4096 }).toDataURL('image/jpeg', 0.8)
|
||||||
|
const { width, height } = cropper.getCropBoxData()
|
||||||
|
const { preview_url } = await api.material.uploadBase64({ file: newImg })
|
||||||
|
context.emit('done', { newImg: preview_url, data: cropper.getData(), width, height })
|
||||||
|
cancel()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
const cancel = () => {
|
||||||
|
store.commit('setShowMoveable', true)
|
||||||
|
dialogVisible.value = false
|
||||||
|
state.url = ''
|
||||||
|
cropData = null
|
||||||
|
state.loading = false
|
||||||
|
cropper.destroy()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
dialogVisible,
|
||||||
|
handleClose,
|
||||||
|
open,
|
||||||
|
ok,
|
||||||
|
cancel,
|
||||||
|
imgBox,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
172
src/components/business/image-cutout/ImageCutout.vue
Normal file
172
src/components/business/image-cutout/ImageCutout.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2023-07-11 23:50:22
|
||||||
|
* @Description: 抠图组件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-14 09:10:31
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="show" title="AI抠图(测试版)" width="650" @close="handleClose">
|
||||||
|
<uploader v-if="!rawImage" :hold="true" :drag="true" :multiple="true" class="uploader" @load="selectFile">
|
||||||
|
<div class="uploader__box">
|
||||||
|
<upload-filled style="width: 64px; height: 64px" />
|
||||||
|
<div class="el-upload__text">在此拖入或选择<em>上传图片</em></div>
|
||||||
|
</div>
|
||||||
|
<div class="el-upload__tip">支持 jpg 或 png 格式的文件,大小不超过 2M</div>
|
||||||
|
</uploader>
|
||||||
|
<div class="content">
|
||||||
|
<div v-show="rawImage" v-loading="!cutImage" :style="{ width: offsetWidth ? offsetWidth + 'px' : '100%' }" class="scan-effect">
|
||||||
|
<img ref="raw" :src="rawImage" alt="" />
|
||||||
|
<div :style="{ right: 100 - percent + '%' }" class="bg"></div>
|
||||||
|
<img v-show="cutImage" :src="cutImage" alt="结果图像" @mousemove="mousemove" />
|
||||||
|
<div v-show="cutImage" :style="{ left: percent + '%' }" class="scan-line"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button v-show="rawImage" @click="clear">重新选择图片</el-button>
|
||||||
|
<el-button v-show="cutImage" type="primary" plain @click="download"> 下载 </el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, onMounted, nextTick } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
// import { client } from '@gradio/client'
|
||||||
|
// import * as api from '@/api/ai'
|
||||||
|
import uploader from '@/components/common/Uploader/index.vue'
|
||||||
|
import _dl from '@/common/methods/download'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { uploader, UploadFilled },
|
||||||
|
setup() {
|
||||||
|
const store = useStore()
|
||||||
|
const state: any = reactive({
|
||||||
|
show: false,
|
||||||
|
rawImage: '',
|
||||||
|
cutImage: '',
|
||||||
|
raw: null,
|
||||||
|
offsetWidth: 0,
|
||||||
|
percent: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
let app: any = null
|
||||||
|
let fileName: string = 'unknow'
|
||||||
|
let isRuning: boolean = false
|
||||||
|
onMounted(async () => {
|
||||||
|
app = await store.getters.app
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectFile = async (file: File) => {
|
||||||
|
if (file.size > 1024 * 1024 * 2) {
|
||||||
|
alert('上传的文件大小超过了限制!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 显示选择的图片
|
||||||
|
state.raw.addEventListener('load', () => {
|
||||||
|
state.offsetWidth = state.raw.offsetWidth
|
||||||
|
})
|
||||||
|
state.rawImage = URL.createObjectURL(file)
|
||||||
|
fileName = file.name
|
||||||
|
// 返回抠图
|
||||||
|
const result = await app.predict('/predict', [
|
||||||
|
file, // blob in 'Input' Image component
|
||||||
|
'u2netp', // string (Option from: ['isnet-anime', 'isnet-general-use', 'sam', 'silueta', 'u2net', 'u2net_cloth_seg', 'u2net_custom', 'u2net_human_seg', 'u2netp']) in 'Models' Dropdown component
|
||||||
|
'',
|
||||||
|
])
|
||||||
|
state.rawImage && (state.cutImage = result?.data[0])
|
||||||
|
requestAnimationFrame(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
state.show = true
|
||||||
|
store.commit('setShowMoveable', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
store.commit('setShowMoveable', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousemove = (e: MouseEvent) => {
|
||||||
|
!isRuning && (state.percent = (e.offsetX / (e.target as any).width) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const download = () => {
|
||||||
|
_dl.downloadBase64File(state.cutImage, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
URL.revokeObjectURL(state.rawImage)
|
||||||
|
state.rawImage = ''
|
||||||
|
// URL.revokeObjectURL(state.cutImage)
|
||||||
|
state.cutImage = ''
|
||||||
|
state.percent = 0
|
||||||
|
state.offsetWidth = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
state.percent += 1
|
||||||
|
isRuning = true
|
||||||
|
state.percent < 100 ? requestAnimationFrame(run) : (isRuning = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clear,
|
||||||
|
download,
|
||||||
|
mousemove,
|
||||||
|
selectFile,
|
||||||
|
open,
|
||||||
|
handleClose,
|
||||||
|
...toRefs(state),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.uploader {
|
||||||
|
&__box {
|
||||||
|
color: #333333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.scan-effect {
|
||||||
|
position: relative;
|
||||||
|
height: 50vh;
|
||||||
|
overflow: hidden;
|
||||||
|
img {
|
||||||
|
// width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 1.5px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
// background-image: linear-gradient(to top, transparent, rgba(255, 255, 255, 0.7), transparent);
|
||||||
|
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
9
src/components/business/image-cutout/index.ts
Normal file
9
src/components/business/image-cutout/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-10-08 10:07:10
|
||||||
|
* @Description: 图像抠图
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 00:05:48
|
||||||
|
*/
|
||||||
|
import index from './ImageCutout.vue'
|
||||||
|
export default index
|
500
src/components/business/moveable/Moveable.vue
Normal file
500
src/components/business/moveable/Moveable.vue
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-04 11:46:39
|
||||||
|
* @Description: 原版movable插件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-17 11:53:30
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div id="empty" class="moveable__remove-item zk-moveable-style"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, nextTick } from 'vue'
|
||||||
|
|
||||||
|
import Moveable, { EVENTS } from 'moveable' // PROPERTIES, METHODS,
|
||||||
|
import MoveableHelper from 'moveable-helper'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
// import { setTransformAttribute } from '@/common/methods/handleTransform'
|
||||||
|
import useSelecto from './Selecto'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {},
|
||||||
|
computed: mapGetters(['dSelectWidgets', 'dActiveElement', 'activeMouseEvent', 'showMoveable', 'showRotatable', 'dWidgets', 'updateRect', 'updateSelect', 'guidelines']),
|
||||||
|
watch: {
|
||||||
|
async dActiveElement(val) {
|
||||||
|
if (!val.record) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 选中非面板 并且不是组合内的元素
|
||||||
|
if (val.uuid != -1) {
|
||||||
|
await nextTick()
|
||||||
|
const target = `[id="${val.uuid}"]`
|
||||||
|
this._target = `[id="${val.uuid}"]`
|
||||||
|
this.moveable.rotatable = true // 选择时会取消旋转
|
||||||
|
// 方向点位设置
|
||||||
|
// this.moveable.renderDirections = val.type === 'w-text' ? ['e', 'se'] : 'w-image' ? ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'] : ['nw', 'ne', 'sw', 'se']
|
||||||
|
switch (val.type) {
|
||||||
|
case 'w-text':
|
||||||
|
this.moveable.renderDirections = ['e', 'se']
|
||||||
|
break
|
||||||
|
case 'w-image':
|
||||||
|
this.moveable.renderDirections = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']
|
||||||
|
break
|
||||||
|
// case 'w-svg':
|
||||||
|
// this.moveable.renderDirections = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']
|
||||||
|
// break
|
||||||
|
default:
|
||||||
|
this.moveable.renderDirections = ['nw', 'ne', 'sw', 'se']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// // Set Move Auto
|
||||||
|
this.moveable.setState({ target: this._target }, () => {
|
||||||
|
// 当出现mouseevent时进行即刻选中
|
||||||
|
if (this.activeMouseEvent) {
|
||||||
|
this.moveable.dragStart(this.activeMouseEvent)
|
||||||
|
// TODO 使用后销毁mouseevent
|
||||||
|
this.$store.commit('setMouseEvent', null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// // End
|
||||||
|
this.$store.commit('setShowMoveable', true)
|
||||||
|
// 参考线设置
|
||||||
|
if (!this.moveable.elementGuidelines.includes(target)) {
|
||||||
|
this.moveable.elementGuidelines.push(target)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.moveable.target = `[id="empty"]`
|
||||||
|
if (this.moveable.target !== `[id="empty"]`) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.moveable.target = `[id="empty"]`
|
||||||
|
}, 210)
|
||||||
|
}
|
||||||
|
// feature: 可以遍历来设置参考线,目前先粗暴清空
|
||||||
|
this.moveable.elementGuidelines.length = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMoveable(val) {
|
||||||
|
if (val) {
|
||||||
|
this.moveable.target = this._target
|
||||||
|
} else {
|
||||||
|
this.moveable.target = `[id="empty"]`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showRotatable(val) {
|
||||||
|
// TODO: 这里是通过旋转来判断是否可以操作
|
||||||
|
this.moveable.renderDirections = val ? ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'] : []
|
||||||
|
this.moveable.resizable = val
|
||||||
|
this.moveable.scalable = val
|
||||||
|
document.getElementsByClassName('moveable-rotation')[0].style.display = val ? 'block' : 'none'
|
||||||
|
},
|
||||||
|
updateRect(val) {
|
||||||
|
this.moveable.updateRect()
|
||||||
|
},
|
||||||
|
updateSelect() {
|
||||||
|
const items = this.$store.getters.dSelectWidgets
|
||||||
|
setTimeout(async () => {
|
||||||
|
this.moveable.updateRect()
|
||||||
|
await this.$nextTick()
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
console.log(items[i].uuid)
|
||||||
|
|
||||||
|
document.getElementById(items[i].uuid)?.classList.add('widget-selected')
|
||||||
|
}
|
||||||
|
this.moveable.renderDirections = []
|
||||||
|
this.moveable.rotatable = false
|
||||||
|
const targetCollector = [].slice.call(document.querySelectorAll('.widget-selected'))
|
||||||
|
console.log(targetCollector)
|
||||||
|
|
||||||
|
this.moveable.target = targetCollector
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
document.getElementById(items[i].uuid)?.classList.remove('widget-selected')
|
||||||
|
}
|
||||||
|
}, 400)
|
||||||
|
},
|
||||||
|
// 选择的元素
|
||||||
|
dSelectWidgets: {
|
||||||
|
handler(items) {
|
||||||
|
const alt = this.$store.getters.dAltDown
|
||||||
|
// if (items.length > 1) {
|
||||||
|
// console.log('打开组合面板')
|
||||||
|
// }
|
||||||
|
if (alt) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
document.getElementById(items[i].uuid)?.classList.add('widget-selected')
|
||||||
|
}
|
||||||
|
this.moveable.renderDirections = []
|
||||||
|
this.moveable.rotatable = false
|
||||||
|
const targetCollector = [].slice.call(document.querySelectorAll('.widget-selected'))
|
||||||
|
// this.moveable.target = `[id="empty"]`
|
||||||
|
this.moveable.target = targetCollector
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
document.getElementById(items[i].uuid)?.classList.remove('widget-selected')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
// 标尺线
|
||||||
|
guidelines(lines) {
|
||||||
|
console.log(lines)
|
||||||
|
|
||||||
|
this.moveable.verticalGuidelines = lines.verticalGuidelines
|
||||||
|
this.moveable.horizontalGuidelines = lines.horizontalGuidelines
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let holdGroupPosition: any = null
|
||||||
|
const moveableOptions: any = {
|
||||||
|
target: document.querySelector(`[id="empty"]`),
|
||||||
|
// container: document.querySelector('#page-design'),
|
||||||
|
zoom: 0.8,
|
||||||
|
draggable: true,
|
||||||
|
clippable: false, // 裁剪
|
||||||
|
throttleDrag: 0,
|
||||||
|
resizable: true,
|
||||||
|
throttleResize: 0,
|
||||||
|
scalable: false,
|
||||||
|
throttleScale: 0,
|
||||||
|
keepRatio: true,
|
||||||
|
rotatable: true,
|
||||||
|
throttleRotate: 0,
|
||||||
|
renderDirections: ['nw', 'ne', 'sw', 'se'], // ['nw', 'ne', 'sw', 'se'] // 'e'
|
||||||
|
pinchable: true, // ["draggable", "resizable", "scalable", "rotatable"]
|
||||||
|
origin: false,
|
||||||
|
defaultGroupOrigin: '0% 0%',
|
||||||
|
// 样式相关
|
||||||
|
rotationPosition: 'bottom',
|
||||||
|
className: 'zk-moveable-style',
|
||||||
|
// -- 吸附对齐 Start --
|
||||||
|
snappable: true,
|
||||||
|
elementGuidelines: [],
|
||||||
|
verticalGuidelines: [],
|
||||||
|
horizontalGuidelines: [],
|
||||||
|
snapThreshold: 4,
|
||||||
|
isDisplaySnapDigit: true,
|
||||||
|
snapGap: false,
|
||||||
|
snapElement: true,
|
||||||
|
snapVertical: true,
|
||||||
|
snapHorizontal: true,
|
||||||
|
snapCenter: false,
|
||||||
|
snapDigit: 0,
|
||||||
|
|
||||||
|
// snapDirections={{"top":true,"right":true,"bottom":true,"left":true}}
|
||||||
|
// elementSnapDirections={{}}
|
||||||
|
// -- END --
|
||||||
|
triggerAblesSimultaneously: true,
|
||||||
|
}
|
||||||
|
const moveable = new Moveable(document.body, moveableOptions)
|
||||||
|
this.moveable = moveable
|
||||||
|
|
||||||
|
const helper: any = new MoveableHelper()
|
||||||
|
|
||||||
|
EVENTS.forEach((event) => {
|
||||||
|
let helperEvent = event.replace(event[0], 'on' + event[0].toUpperCase())
|
||||||
|
// console.log(event)
|
||||||
|
// 'resizeStart', 'resize', 'resizeEnd', rotate, onScale, onScaleStart
|
||||||
|
if (['resizeStart', 'rotate', 'resize'].includes(event)) {
|
||||||
|
moveable.on(event, (...args) => {
|
||||||
|
// this.$emit(event, ...args)
|
||||||
|
helper[helperEvent] && helper[helperEvent](...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/* draggable */
|
||||||
|
let resizeStartWidth = 0
|
||||||
|
|
||||||
|
moveable
|
||||||
|
.on('dragStart', ({ inputEvent, target, stop }) => {
|
||||||
|
if (inputEvent.target.nodeName === 'PRE') {
|
||||||
|
this.dActiveElement.editable && stop()
|
||||||
|
}
|
||||||
|
this.dActiveElement.lock && stop()
|
||||||
|
})
|
||||||
|
.on('drag', ({ target, transform, left, top, inputEvent }) => {
|
||||||
|
// target!.style.transform = transform]
|
||||||
|
target!.style.left = `${left}px`
|
||||||
|
target!.style.top = `${top}px`
|
||||||
|
this.holdPosition = { left, top }
|
||||||
|
})
|
||||||
|
.on('dragEnd', ({ target, isDrag, inputEvent }) => {
|
||||||
|
// console.log('onDragEnd', inputEvent)
|
||||||
|
// TODO 清理mouseevent
|
||||||
|
this.$store.commit('setMouseEvent', null)
|
||||||
|
inputEvent.stopPropagation()
|
||||||
|
inputEvent.preventDefault()
|
||||||
|
// console.log(this.holdPosition, inputEvent.pageX, inputEvent.pageY)
|
||||||
|
if (this.holdPosition) {
|
||||||
|
this.updateWidgetData({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
key: 'left',
|
||||||
|
value: this.holdPosition?.left,
|
||||||
|
})
|
||||||
|
this.updateWidgetData({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
key: 'top',
|
||||||
|
value: this.holdPosition?.top,
|
||||||
|
})
|
||||||
|
this.holdPosition = null // important
|
||||||
|
setTimeout(() => {
|
||||||
|
this.pushHistory()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// .on('keyUp', (e) => {
|
||||||
|
// moveable.updateRect()
|
||||||
|
// })
|
||||||
|
.on('rotate', ({ target, beforeDist, dist, transform }: any) => {
|
||||||
|
// console.log('onRotate', Number(this.dActiveElement.rotate) + Number(beforeDist + dist))
|
||||||
|
// target.style.transform = transform
|
||||||
|
console.log(target.style.transform)
|
||||||
|
})
|
||||||
|
.on('rotateEnd', (e: any) => {
|
||||||
|
const tf = e.target.style.transform
|
||||||
|
const iof = tf.indexOf('rotate')
|
||||||
|
let rotate = ''
|
||||||
|
if (iof != -1) {
|
||||||
|
const index = iof + 'rotate'.length
|
||||||
|
const half = tf.substring(index + 1)
|
||||||
|
rotate = half.slice(0, half.indexOf(')'))
|
||||||
|
}
|
||||||
|
rotate &&
|
||||||
|
this.updateWidgetData({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
key: 'rotate',
|
||||||
|
value: rotate,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on('resizeStart', (args) => {
|
||||||
|
console.log(args.target.style.transform)
|
||||||
|
|
||||||
|
this.moveable.snappable = false
|
||||||
|
if (this.dActiveElement.type === 'w-text') {
|
||||||
|
if (String(args.direction) === '1,0') {
|
||||||
|
moveable.keepRatio = false
|
||||||
|
moveable.scalable = false
|
||||||
|
}
|
||||||
|
if (String(args.direction) === '1,1') {
|
||||||
|
moveable.keepRatio = false
|
||||||
|
resizeStartWidth = args.target.offsetWidth
|
||||||
|
this.startHL = Number(args.target!.style.lineHeight.replace('px', ''))
|
||||||
|
this.startLS = Number(args.target!.style.letterSpacing.replace('px', ''))
|
||||||
|
this.resetRatio = 1
|
||||||
|
}
|
||||||
|
} else if (this.dActiveElement.type === 'w-image' || this.dActiveElement.type === 'w-qrcode' || this.dActiveElement.type === 'w-svg') {
|
||||||
|
const dirs = ['1,0', '0,-1', '-1,0', '0,1']
|
||||||
|
dirs.includes(String(args.direction)) && (moveable.keepRatio = false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('resize', (args: any) => {
|
||||||
|
const { target, width, height, dist, delta, clientX, clientY, direction } = args
|
||||||
|
console.log(2, args)
|
||||||
|
if (this.dActiveElement.type === 'w-text') {
|
||||||
|
if (String(direction) === '1,1') {
|
||||||
|
this.resetRatio = width / resizeStartWidth
|
||||||
|
target!.style.fontSize = this.dActiveElement.fontSize * this.resetRatio + 'px'
|
||||||
|
target!.style.letterSpacing = this.startLS * this.resetRatio + 'px'
|
||||||
|
target!.style.lineHeight = this.startHL * this.resetRatio + 'px'
|
||||||
|
}
|
||||||
|
target.style.width = width
|
||||||
|
target.style.height = height
|
||||||
|
this.resizeTempData = { width, height }
|
||||||
|
// moveable.updateRect()
|
||||||
|
target.style.backgroundImage = 'none'
|
||||||
|
// moveable.keepRatio !== this.resetRatio > 1 && (moveable.keepRatio = this.resetRatio > 1)
|
||||||
|
} else if (this.dActiveElement.type == 'w-image' || this.dActiveElement.type === 'w-qrcode' || this.dActiveElement.type === 'w-svg') {
|
||||||
|
this.resizeTempData = { width, height }
|
||||||
|
} else if (this.dActiveElement.type == 'w-group') {
|
||||||
|
// let record = this.dActiveElement.record
|
||||||
|
// this.dActiveElement.tempScale = width / record.width
|
||||||
|
this.$store.commit('resize', { width: width, height: height })
|
||||||
|
// this.resizeTempData = { width, height }
|
||||||
|
// let record = this.dActiveElement.record
|
||||||
|
// setTransformAttribute(target, 'scale', width / record.width)
|
||||||
|
} else {
|
||||||
|
this.$store.commit('resize', { width: width, height: height })
|
||||||
|
}
|
||||||
|
this.dActiveElement.rotate && (target!.style.transform = target!.style.transform.replace('(0deg', `(${this.dActiveElement.rotate}`))
|
||||||
|
})
|
||||||
|
.on('resizeEnd', (e: any) => {
|
||||||
|
moveable.resizable = true
|
||||||
|
// moveable.scalable = true
|
||||||
|
moveable.snappable = true
|
||||||
|
if (e.lastEvent) {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// if (this.dActiveElement.type === 'w-group') {
|
||||||
|
// // 临时屏蔽,抖得太严重
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
console.log('重置translate', this.dActiveElement)
|
||||||
|
// 转换成位置
|
||||||
|
// if (this.dActiveElement.cache && this.dActiveElement.cache.recordLeft) {
|
||||||
|
// const left = e.lastEvent.drag.translate[0] + Number(this.dActiveElement.cache.recordLeft)
|
||||||
|
// const top = e.lastEvent.drag.translate[1] + Number(this.dActiveElement.cache.recordTop)
|
||||||
|
// this.dActiveElement.cache = { left, top }
|
||||||
|
// } else {
|
||||||
|
// const left = e.lastEvent.drag.translate[0] + Number(this.dActiveElement.left)
|
||||||
|
// const top = e.lastEvent.drag.translate[1] + Number(this.dActiveElement.top)
|
||||||
|
// this.dActiveElement.cache = { left, top }
|
||||||
|
// }
|
||||||
|
const left = e.lastEvent.drag.translate[0]
|
||||||
|
const top = e.lastEvent.drag.translate[1]
|
||||||
|
this.updateWidgetMultiple({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
key: 'left',
|
||||||
|
value: Number(this.dActiveElement.left) + left,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'top',
|
||||||
|
value: Number(this.dActiveElement.top) + top,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
// 重置translate
|
||||||
|
const tf = e.target.style.transform
|
||||||
|
const iof = tf.indexOf('translate')
|
||||||
|
const FRONT = tf.slice(0, iof + 'translate'.length + 1)
|
||||||
|
const half = tf.substring(iof + 'translate'.length + 1)
|
||||||
|
const END = half.substring(half.indexOf(')'))
|
||||||
|
e.target.style.transform = FRONT + '0, 0' + END
|
||||||
|
// this.moveable.updateRect()
|
||||||
|
// }, 10)
|
||||||
|
}
|
||||||
|
if (this.resizeTempData) {
|
||||||
|
this.$store.commit('resize', this.resizeTempData)
|
||||||
|
this.resizeTempData = null
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.$nextTick()
|
||||||
|
this.moveable.updateRect()
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (this.dActiveElement.type === 'w-text') {
|
||||||
|
const d = e.direction || e.lastEvent.direction
|
||||||
|
String(d) === '1,1' && (this.dActiveElement.fontSize = this.dActiveElement.fontSize * this.resetRatio)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
moveable.keepRatio = true
|
||||||
|
})
|
||||||
|
.on('scaleStart', (e) => {
|
||||||
|
if (this.dActiveElement.type === 'w-text') {
|
||||||
|
this.startHL = Number(e.target!.style.lineHeight.replace('px', ''))
|
||||||
|
this.startLS = Number(e.target!.style.letterSpacing.replace('px', ''))
|
||||||
|
this.resetRatio = 1
|
||||||
|
} else {
|
||||||
|
moveable.scalable = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('scale', (e) => {
|
||||||
|
moveable.resizable = false
|
||||||
|
const { target, scale, transform } = e
|
||||||
|
this.resetRatio = scale[0]
|
||||||
|
target!.style.transform = transform
|
||||||
|
this.dActiveElement.rotate && (target!.style.transform = target!.style.transform.replace('0deg', this.dActiveElement.rotate))
|
||||||
|
})
|
||||||
|
.on('scaleEnd', (e) => {
|
||||||
|
moveable.resizable = true
|
||||||
|
// moveable.scalable = true
|
||||||
|
moveable.keepRatio = true
|
||||||
|
console.log(e.target.style.transform)
|
||||||
|
try {
|
||||||
|
if (this.dActiveElement.type === 'w-text') {
|
||||||
|
const d = e.direction || e.lastEvent.direction
|
||||||
|
String(d) === '1,1' && (this.dActiveElement.fontSize = this.dActiveElement.fontSize * this.resetRatio)
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
})
|
||||||
|
.on('dragGroup', (e) => {
|
||||||
|
e.inputEvent.stopPropagation()
|
||||||
|
e.inputEvent.preventDefault()
|
||||||
|
holdGroupPosition = {}
|
||||||
|
const events = e.events
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
const ev = events[i]
|
||||||
|
const currentWidget = this.dWidgets.find((item: any) => item.uuid === ev.target.getAttribute('data-uuid'))
|
||||||
|
const left = Number(currentWidget.left) + ev.beforeTranslate[0]
|
||||||
|
// debug -- start --
|
||||||
|
if (i === 1) {
|
||||||
|
console.log(Number(currentWidget.left), ev.beforeTranslate[0])
|
||||||
|
}
|
||||||
|
// debug -- end --
|
||||||
|
const top = Number(currentWidget.top) + ev.beforeTranslate[1]
|
||||||
|
ev.target.style.left = `${left}px`
|
||||||
|
ev.target.style.top = `${top}px`
|
||||||
|
holdGroupPosition[`${ev.target.getAttribute('data-uuid')}`] = { left, top }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('dragGroupEnd', (e) => {
|
||||||
|
for (const key in holdGroupPosition) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(holdGroupPosition, key)) {
|
||||||
|
const item = holdGroupPosition[key]
|
||||||
|
this.updateWidgetData({
|
||||||
|
uuid: key,
|
||||||
|
key: 'left',
|
||||||
|
value: item.left,
|
||||||
|
})
|
||||||
|
this.updateWidgetData({
|
||||||
|
uuid: key,
|
||||||
|
key: 'top',
|
||||||
|
value: item.top,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holdGroupPosition = null
|
||||||
|
// background: linear-gradient(to right, #ccc 0%, #ccc 50%, transparent 50%);
|
||||||
|
// background-size: 12px 1px;
|
||||||
|
})
|
||||||
|
.on('resizeGroupStart', ({ events }: any) => {
|
||||||
|
console.log(events)
|
||||||
|
// events.forEach((ev, i) => {
|
||||||
|
// const frame = this.frames[i];
|
||||||
|
// // Set origin if transform-origin use %.
|
||||||
|
// ev.setOrigin(["%", "%"]);
|
||||||
|
|
||||||
|
// // If cssSize and offsetSize are different, set cssSize.
|
||||||
|
// const style = window.getComputedStyle(ev.target);
|
||||||
|
// const cssWidth = parseFloat(style.width);
|
||||||
|
// const cssHeight = parseFloat(style.height);
|
||||||
|
// ev.set([cssWidth, cssHeight]);
|
||||||
|
|
||||||
|
// // If a drag event has already occurred, there is no dragStart.
|
||||||
|
// ev.dragStart && ev.dragStart.set(frame.translate);
|
||||||
|
// });
|
||||||
|
})
|
||||||
|
.on('resizeGroup', (e: any) => {
|
||||||
|
// events.forEach(({ target, width, height, drag }, i) => {
|
||||||
|
// const frame = this.frames[i];
|
||||||
|
// target.style.width = `${width}px`;
|
||||||
|
// target.style.height = `${height}px`;
|
||||||
|
// // get drag event
|
||||||
|
// frame.translate = drag.beforeTranslate;
|
||||||
|
// target.style.transform
|
||||||
|
// = `translate(${drag.beforeTranslate[0]}px, ${drag.beforeTranslate[1]}px)`;
|
||||||
|
// });
|
||||||
|
})
|
||||||
|
.on('resizeGroupEnd', ({ targets, isDrag }: any) => {
|
||||||
|
console.log('onResizeGroupEnd', targets, isDrag)
|
||||||
|
})
|
||||||
|
|
||||||
|
// -- 选择功能 Start --
|
||||||
|
useSelecto(this.moveable)
|
||||||
|
// -- 选择功能 END --
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await nextTick()
|
||||||
|
const Ele = document.getElementById('page-design')
|
||||||
|
// 后续可能加个节流 TODO
|
||||||
|
Ele?.addEventListener('scroll', () => {
|
||||||
|
this.moveable.updateRect()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['updateWidgetData', 'updateWidgetMultiple', 'pushHistory']),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import url('./style/index.less');
|
||||||
|
</style>
|
41
src/components/business/moveable/Selecto.ts
Normal file
41
src/components/business/moveable/Selecto.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Selecto from 'selecto'
|
||||||
|
import { getElementInfo } from 'moveable'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
|
export default function(moveable: any) {
|
||||||
|
const selecto = new Selecto({
|
||||||
|
container: document.getElementById('page-design'),
|
||||||
|
selectableTargets: ['.layer'],
|
||||||
|
selectByClick: false,
|
||||||
|
// 是否从内部目标中选择(default: true)
|
||||||
|
selectFromInside: false,
|
||||||
|
// 选择后,是否与所选目标一起选择下一个目标
|
||||||
|
continueSelect: false,
|
||||||
|
// Determines which key to continue selecting the next target via keydown and keyup.
|
||||||
|
toggleContinueSelect: 'shift',
|
||||||
|
// The container for keydown and keyup events
|
||||||
|
keyContainer: document.getElementById('page-design'),
|
||||||
|
// 目标与要选择的拖动区域重叠的速率。(默认:100)
|
||||||
|
hitRate: 5,
|
||||||
|
getElementRect: getElementInfo,
|
||||||
|
})
|
||||||
|
selecto.on('select', (e) => {
|
||||||
|
e.added.forEach((el) => {
|
||||||
|
if (!Array.from(el.classList).includes('layer-lock') && !el.hasAttribute('child')) {
|
||||||
|
el.classList.add('widget-selected')
|
||||||
|
store.dispatch('selectWidgetsInOut', {
|
||||||
|
uuid: el.getAttribute('data-uuid'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
e.removed.forEach((el) => {
|
||||||
|
el.classList.remove('widget-selected')
|
||||||
|
store.dispatch('selectWidgetsInOut', {
|
||||||
|
uuid: el.getAttribute('data-uuid'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
moveable.renderDirections = [] // ['nw', 'ne', 'sw', 'se'] // []
|
||||||
|
moveable.rotatable = false
|
||||||
|
moveable.target = [].slice.call(document.querySelectorAll('.widget-selected'))
|
||||||
|
})
|
||||||
|
}
|
70
src/components/business/moveable/style/index.less
Normal file
70
src/components/business/moveable/style/index.less
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 2层 .zk-moveable-style 增加css权重层级,避免太多 important
|
||||||
|
.zk-moveable-style.zk-moveable-style {
|
||||||
|
--moveable-color: #6ccfff;
|
||||||
|
|
||||||
|
// 缩放圆点
|
||||||
|
.moveable-control {
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #c0c5cf;
|
||||||
|
box-shadow: 0 0 2px 0 rgb(86, 90, 98, 0.2);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-left: -6px;
|
||||||
|
|
||||||
|
// &.moveable-n,
|
||||||
|
// &.moveable-s,
|
||||||
|
// &.moveable-e,
|
||||||
|
// &.moveable-w {
|
||||||
|
// display: none;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 上下缩放点
|
||||||
|
&.moveable-n,
|
||||||
|
&.moveable-s {
|
||||||
|
width: 16px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: -8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
// 左右缩放点
|
||||||
|
&.moveable-e,
|
||||||
|
&.moveable-w {
|
||||||
|
width: 8px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-top: -8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旋转按钮
|
||||||
|
.moveable-rotation {
|
||||||
|
height: 35px;
|
||||||
|
display: block;
|
||||||
|
.moveable-rotation-control {
|
||||||
|
border: none;
|
||||||
|
background-image: url('./rotation-icon.svg');
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
display: block;
|
||||||
|
margin-left: -11px;
|
||||||
|
// margin-top: -11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旋转的操作条
|
||||||
|
.moveable-rotation-line {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.moveable__remove-item {
|
||||||
|
position: fixed;
|
||||||
|
left: -9999px;
|
||||||
|
top: -9999px;
|
||||||
|
}
|
1
src/components/business/moveable/style/rotation-icon.svg
Normal file
1
src/components/business/moveable/style/rotation-icon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='#757575'><g fill='none' fill-rule='evenodd'><circle stroke='#CCD1DA' fill='#FFF' cx='12' cy='12' r='11.5'/><path d='M16.242 12.012a4.25 4.25 0 00-5.944-4.158L9.696 6.48a5.75 5.75 0 018.048 5.532h1.263l-2.01 3.002-2.008-3.002h1.253zm-8.484-.004a4.25 4.25 0 005.943 3.638l.6 1.375a5.75 5.75 0 01-8.046-5.013H5.023L7.02 9.004l1.997 3.004h-1.26z' fill='#000' fill-rule='nonzero'/></g></svg>
|
After Width: | Height: | Size: 455 B |
9
src/components/business/picture-selector/index.ts
Normal file
9
src/components/business/picture-selector/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-10-08 10:07:10
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-26 20:48:29
|
||||||
|
*/
|
||||||
|
import index from './index.vue'
|
||||||
|
export default index
|
100
src/components/business/picture-selector/index.vue
Normal file
100
src/components/business/picture-selector/index.vue
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-10-08 10:07:19
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-29 17:57:46
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="选择图片" @close="close">
|
||||||
|
<el-tabs tab-position="left" style="height: 60vh" class="demo-tabs">
|
||||||
|
<el-tab-pane label="个人素材">
|
||||||
|
<div class="pic__box">
|
||||||
|
<photo-list ref="imgListComp" :isDone="isDone" :listData="imgList" @load="load" @select="selectImg" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="照片库"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<!-- <template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">Cancel</el-button>
|
||||||
|
<el-button plain type="primary" @click="dialogVisible = false"
|
||||||
|
>Confirm</el-button
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</template> -->
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, toRefs, reactive } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { ElTabPane, ElTabs } from 'element-plus'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { [ElTabPane.name]: ElTabPane, [ElTabs.name]: ElTabs },
|
||||||
|
emits: ['select'],
|
||||||
|
setup(props, context) {
|
||||||
|
const store = useStore()
|
||||||
|
const state: any = reactive({
|
||||||
|
dialogVisible: false,
|
||||||
|
imgList: [],
|
||||||
|
isDone: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
let loading = false
|
||||||
|
let page = 0
|
||||||
|
let listPage = 0
|
||||||
|
const load = (init?: boolean) => {
|
||||||
|
if (init) {
|
||||||
|
state.imgList = []
|
||||||
|
page = 0
|
||||||
|
state.isDone = false
|
||||||
|
}
|
||||||
|
if (state.isDone || loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading = true
|
||||||
|
page += 1
|
||||||
|
api.material.getMyPhoto({ page }).then(({ list }: any) => {
|
||||||
|
list.length <= 0 ? (state.isDone = true) : (state.imgList = state.imgList.concat(list))
|
||||||
|
setTimeout(() => {
|
||||||
|
loading = false
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
state.dialogVisible = true
|
||||||
|
load()
|
||||||
|
store.commit('setShowMoveable', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
store.commit('setShowMoveable', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectImg = (index: number) => {
|
||||||
|
const item: any = state.imgList[index]
|
||||||
|
context.emit('select', item)
|
||||||
|
state.dialogVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
load,
|
||||||
|
selectImg,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.pic__box {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
|
</style>
|
9
src/components/business/qrcode/index.ts
Normal file
9
src/components/business/qrcode/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-03-16 09:14:43
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-03-16 09:16:19
|
||||||
|
*/
|
||||||
|
import index from './index.vue'
|
||||||
|
export default index
|
109
src/components/business/qrcode/index.vue
Normal file
109
src/components/business/qrcode/index.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-03-16 09:15:52
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-08 17:53:00
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div ref="qrCodeDom" class="qrcode__wrap"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, ref, watch, nextTick } from 'vue'
|
||||||
|
import QRCodeStyling, { DrawType, TypeNumber, Mode, ErrorCorrectionLevel, DotType, CornerSquareType, CornerDotType, Extension } from 'qr-code-styling'
|
||||||
|
import { debounce } from 'throttle-debounce'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
width: {
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
image: {},
|
||||||
|
value: {},
|
||||||
|
dotsOptions: {
|
||||||
|
default: () => {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
let options = {}
|
||||||
|
watch(
|
||||||
|
() => [props.width, props.height, props.dotsOptions],
|
||||||
|
() => {
|
||||||
|
render()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const render = debounce(300, false, async () => {
|
||||||
|
options = {
|
||||||
|
width: props.width,
|
||||||
|
height: props.height,
|
||||||
|
type: 'canvas' as DrawType, // canvas svg
|
||||||
|
data: props.value,
|
||||||
|
image: props.image, // /favicon.svg
|
||||||
|
margin: 2,
|
||||||
|
qrOptions: {
|
||||||
|
typeNumber: 3 as TypeNumber,
|
||||||
|
mode: 'Byte' as Mode,
|
||||||
|
errorCorrectionLevel: 'M' as ErrorCorrectionLevel,
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
hideBackgroundDots: true,
|
||||||
|
imageSize: 0.4,
|
||||||
|
margin: 6,
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
},
|
||||||
|
backgroundOptions: {
|
||||||
|
color: '#ffffff',
|
||||||
|
},
|
||||||
|
dotsOptions: {
|
||||||
|
color: '#41b583',
|
||||||
|
type: 'rounded' as DotType,
|
||||||
|
...props.dotsOptions,
|
||||||
|
},
|
||||||
|
cornersSquareOptions: {
|
||||||
|
color: props.dotsOptions.color,
|
||||||
|
type: '',
|
||||||
|
// type: 'extra-rounded' as CornerSquareType,
|
||||||
|
// gradient: {
|
||||||
|
// type: 'linear', // 'radial'
|
||||||
|
// rotation: 180,
|
||||||
|
// colorStops: [{ offset: 0, color: '#25456e' }, { offset: 1, color: '#4267b2' }]
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
cornersDotOptions: {
|
||||||
|
color: props.dotsOptions.color,
|
||||||
|
type: 'square' as CornerDotType,
|
||||||
|
// gradient: {
|
||||||
|
// type: 'linear', // 'radial'
|
||||||
|
// rotation: 180,
|
||||||
|
// colorStops: [{ offset: 0, color: '#00266e' }, { offset: 1, color: '#4060b3' }]
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (props.value) {
|
||||||
|
qrCode.update(options)
|
||||||
|
await nextTick()
|
||||||
|
qrCodeDom.value.firstChild.style = 'width: 100%;' // 强制其适应缩放
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const qrCode = new QRCodeStyling(options)
|
||||||
|
const qrCodeDom = ref<HTMLElement>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
render()
|
||||||
|
qrCode.append(qrCodeDom.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
qrCodeDom,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
41
src/components/business/right-click-menu/rc-menu-data.ts
Normal file
41
src/components/business/right-click-menu/rc-menu-data.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-07-30 17:38:50
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2021-07-30 18:15:22
|
||||||
|
*/
|
||||||
|
export const menuList: any = {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
list: [],
|
||||||
|
}
|
||||||
|
export const widgetMenu = [
|
||||||
|
{
|
||||||
|
type: 'copy',
|
||||||
|
text: '复制',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'paste',
|
||||||
|
text: '粘贴',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'index-up',
|
||||||
|
text: '上移一层',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'index-down',
|
||||||
|
text: '下移一层',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'del',
|
||||||
|
text: '删除',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const pageMenu = [
|
||||||
|
{
|
||||||
|
type: 'paste',
|
||||||
|
text: '粘贴',
|
||||||
|
},
|
||||||
|
]
|
165
src/components/business/right-click-menu/rc-menu.vue
Normal file
165
src/components/business/right-click-menu/rc-menu.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="showMenuBg" id="menu-bg" class="menu-bg" @click="closeMenu">
|
||||||
|
<ul ref="menuList" class="menu-list" :style="styleObj">
|
||||||
|
<li v-for="(item, index) in menuList.list" :key="index" :class="{ 'menu-item': true, 'disable-menu': dCopyElement.length === 0 && item.type === 'paste' }" @click.stop="selectMenu(item.type)">
|
||||||
|
{{ item.text }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import { widgetMenu, pageMenu, menuList } from './rc-menu-data'
|
||||||
|
import { getTarget } from '@/common/methods/target'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menuList,
|
||||||
|
showMenuBg: false,
|
||||||
|
widgetMenu,
|
||||||
|
pageMenu,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dActiveElement', 'dAltDown', 'dWidgets', 'dCopyElement']),
|
||||||
|
styleObj() {
|
||||||
|
return {
|
||||||
|
left: this.menuList.left + 'px',
|
||||||
|
top: this.menuList.top + 'px',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.oncontextmenu = this.mouseRightClick
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['selectWidget', 'copyWidget', 'pasteWidget', 'updateLayerIndex', 'deleteWidget', 'ungroup']),
|
||||||
|
async mouseRightClick(e: any) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
if (this.showMenuBg) {
|
||||||
|
this.showMenuBg = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// let target = e.target
|
||||||
|
let target = await getTarget(e.target)
|
||||||
|
|
||||||
|
let type = target.getAttribute('data-type')
|
||||||
|
if (type) {
|
||||||
|
let uuid = target.getAttribute('data-uuid') // 设置选中元素
|
||||||
|
|
||||||
|
if (uuid !== '-1' && !this.dAltDown) {
|
||||||
|
let widget = this.dWidgets.find((item: any) => item.uuid === uuid)
|
||||||
|
if (widget.parent !== '-1' && widget.parent !== this.dActiveElement.uuid && widget.parent !== this.dActiveElement.parent) {
|
||||||
|
uuid = widget.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.selectWidget({
|
||||||
|
uuid: uuid || '-1',
|
||||||
|
})
|
||||||
|
this.showMenu(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu(e: any) {
|
||||||
|
let isPage = this.dActiveElement.uuid === '-1'
|
||||||
|
this.menuList.list = isPage ? this.pageMenu : this.widgetMenu
|
||||||
|
if (this.dActiveElement.isContainer) {
|
||||||
|
let ungroup = [
|
||||||
|
{
|
||||||
|
type: 'ungroup',
|
||||||
|
text: '取消组合',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
this.menuList.list = ungroup.concat(this.menuList.list)
|
||||||
|
}
|
||||||
|
this.showMenuBg = true
|
||||||
|
// document.getElementById('menu-bg').addEventListener('click', this.closeMenu, false)
|
||||||
|
let mx = e.pageX
|
||||||
|
let my = e.pageY
|
||||||
|
let listWidth = 120
|
||||||
|
if (mx + listWidth > window.innerWidth) {
|
||||||
|
mx -= listWidth
|
||||||
|
}
|
||||||
|
let listHeight = (14 + 10) * this.menuList.list.length + 10
|
||||||
|
if (my + listHeight > window.innerHeight) {
|
||||||
|
my -= listHeight
|
||||||
|
}
|
||||||
|
this.menuList.left = mx
|
||||||
|
this.menuList.top = my
|
||||||
|
},
|
||||||
|
closeMenu() {
|
||||||
|
this.showMenuBg = false
|
||||||
|
// document.getElementById('menu-bg').removeEventListener('click', this.closeMenu, false)
|
||||||
|
},
|
||||||
|
selectMenu(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'copy':
|
||||||
|
this.copyWidget()
|
||||||
|
break
|
||||||
|
case 'paste':
|
||||||
|
if (this.dCopyElement.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.pasteWidget()
|
||||||
|
break
|
||||||
|
case 'index-up':
|
||||||
|
this.updateLayerIndex({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
value: 1,
|
||||||
|
isGroup: this.dActiveElement.isContainer,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'index-down':
|
||||||
|
this.updateLayerIndex({
|
||||||
|
uuid: this.dActiveElement.uuid,
|
||||||
|
value: -1,
|
||||||
|
isGroup: this.dActiveElement.isContainer,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'del':
|
||||||
|
this.deleteWidget()
|
||||||
|
break
|
||||||
|
case 'ungroup':
|
||||||
|
this.ungroup(this.dActiveElement.uuid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.closeMenu()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.menu-bg {
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 99999;
|
||||||
|
.menu-list {
|
||||||
|
background-color: @color-white;
|
||||||
|
box-shadow: 1px 0px 10px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
width: 120px;
|
||||||
|
.menu-item {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 5px 15px;
|
||||||
|
width: 100%;
|
||||||
|
&:hover {
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.menu-item.disable-menu {
|
||||||
|
background-color: @color-white;
|
||||||
|
color: #aaaaaa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
91
src/components/business/save-download/CreateCover.vue
Normal file
91
src/components/business/save-download/CreateCover.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-01 11:12:17
|
||||||
|
* @Description: 前端出图 - 生成封面
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-11 19:47:24
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div id="cover-wrap">
|
||||||
|
<img id="cover" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, watch, getCurrentInstance, ComponentInternalInstance } from 'vue'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import html2canvas from 'html2canvas'
|
||||||
|
import api from '@/api'
|
||||||
|
import Qiniu from '@/common/methods/QiNiu'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: ['modelValue'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, context) {
|
||||||
|
let gridSizeIndex: number = 0
|
||||||
|
|
||||||
|
// function fileOrBlobToDataURL(obj: any, cb: Function) {
|
||||||
|
// let a = new FileReader()
|
||||||
|
// a.readAsDataURL(obj)
|
||||||
|
// a.onload = (e: any) => {
|
||||||
|
// cb(e.target.result)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// function blobToImage(blob: any) {
|
||||||
|
// return new Promise((resolve) => {
|
||||||
|
// fileOrBlobToDataURL(blob, (dataurl: string) => {
|
||||||
|
// resolve(dataurl)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
const { proxy }: any = getCurrentInstance() as ComponentInternalInstance
|
||||||
|
|
||||||
|
async function createCover(cb: any) {
|
||||||
|
const nowGrideSizeIndex = gridSizeIndex
|
||||||
|
const nowZoom = proxy?.dZoom
|
||||||
|
// 取消选中元素
|
||||||
|
proxy?.selectWidget({
|
||||||
|
uuid: '-1',
|
||||||
|
})
|
||||||
|
gridSizeIndex = 0
|
||||||
|
proxy?.updateZoom(100)
|
||||||
|
const opts = {
|
||||||
|
useCORS: true, // 跨域图片
|
||||||
|
scale: 0.2,
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
html2canvas(document.getElementById('page-design-canvas'), opts).then((canvas: any) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
async function (blobObj: Blob) {
|
||||||
|
const result: any = await Qiniu.upload(blobObj, { bucket: 'cloud-design', prePath: 'cover/user' })
|
||||||
|
cb(result)
|
||||||
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
0.1,
|
||||||
|
)
|
||||||
|
gridSizeIndex = nowGrideSizeIndex
|
||||||
|
proxy?.updateZoom(nowZoom)
|
||||||
|
})
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createCover,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dZoom']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['selectWidget', 'updateZoom']),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#cover-wrap {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
}
|
||||||
|
</style>
|
253
src/components/business/save-download/SaveImage.vue
Normal file
253
src/components/business/save-download/SaveImage.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-01 11:12:17
|
||||||
|
* @Description: 前端出图 - 已废弃
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-11 16:58:34
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div v-show="fillInfoing" class="fill-info-wrap">
|
||||||
|
<div v-loading="loading" class="fill-info-content">
|
||||||
|
<div class="fill-info-step">
|
||||||
|
<div id="cover-wrap">
|
||||||
|
<img id="cover" />
|
||||||
|
</div>
|
||||||
|
<div class="publish-btn" @click="saveImg">
|
||||||
|
<span v-show="!publishing">下载图片</span>
|
||||||
|
<i v-show="publishing" class="el-icon-loading"></i>
|
||||||
|
</div>
|
||||||
|
<div class="close-publish" @click="closePublish">关闭</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, toRefs, watch, getCurrentInstance, ComponentInternalInstance } from 'vue'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import html2canvas from 'html2canvas'
|
||||||
|
import api from '@/api'
|
||||||
|
import useLoading from '@/common/methods/loading'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: ['modelValue'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, context) {
|
||||||
|
let gridSizeIndex: number = 0
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
fillInfoing: false,
|
||||||
|
loading: false,
|
||||||
|
publishing: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const closePublish = () => {
|
||||||
|
state.fillInfoing = false
|
||||||
|
context.emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileOrBlobToDataURL(obj: any, cb: Function) {
|
||||||
|
let a = new FileReader()
|
||||||
|
a.readAsDataURL(obj)
|
||||||
|
a.onload = (e: any) => {
|
||||||
|
cb(e.target.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function blobToImage(blob: any) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
fileOrBlobToDataURL(blob, (dataurl: string) => {
|
||||||
|
resolve(dataurl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { proxy }: any = getCurrentInstance() as ComponentInternalInstance
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
// state.loading = true
|
||||||
|
// state.fillInfoing = true
|
||||||
|
let nowGrideSizeIndex = gridSizeIndex
|
||||||
|
let nowZoom = proxy?.dZoom
|
||||||
|
// 取消选中元素
|
||||||
|
proxy?.selectWidget({
|
||||||
|
uuid: '-1',
|
||||||
|
})
|
||||||
|
gridSizeIndex = 0
|
||||||
|
proxy?.updateZoom(100)
|
||||||
|
const opts = {
|
||||||
|
useCORS: true, // 跨域图片
|
||||||
|
scale: 0.4,
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
html2canvas(document.getElementById('page-design-canvas'), opts).then((canvas) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
async (blob: any) => {
|
||||||
|
const data = await blobToImage(blob)
|
||||||
|
document.getElementById('cover').src = data
|
||||||
|
state.loading = false
|
||||||
|
gridSizeIndex = nowGrideSizeIndex
|
||||||
|
proxy?.updateZoom(nowZoom)
|
||||||
|
proxy?.saveImg()
|
||||||
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
0.2,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveImg() {
|
||||||
|
if (state.publishing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.publishing = true
|
||||||
|
let image = new Image()
|
||||||
|
// 解决跨域 Canvas 污染问题
|
||||||
|
image.setAttribute('crossOrigin', 'anonymous')
|
||||||
|
image.onload = function () {
|
||||||
|
let canvas = document.createElement('canvas')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
context?.drawImage(image, 0, 0, image.width, image.height)
|
||||||
|
let url = canvas.toDataURL('image/png')
|
||||||
|
|
||||||
|
let a = document.createElement('a')
|
||||||
|
let event = new MouseEvent('click')
|
||||||
|
|
||||||
|
// 将a的download属性设置为我们想要下载的图片名称,若name不存在则使用‘ ’作为默认名称
|
||||||
|
a.download = name || '生成图片'
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
// 触发a的单击事件
|
||||||
|
a.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
image.src = document.getElementById('cover').src
|
||||||
|
state.publishing = false
|
||||||
|
state.fillInfoing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createImg(cb: any) {
|
||||||
|
let loading: any = useLoading('保存封面中')
|
||||||
|
let nowGrideSizeIndex = gridSizeIndex
|
||||||
|
let nowZoom = proxy?.dZoom
|
||||||
|
// 取消选中元素
|
||||||
|
proxy?.selectWidget({
|
||||||
|
uuid: '-1',
|
||||||
|
})
|
||||||
|
gridSizeIndex = 0
|
||||||
|
proxy?.updateZoom(100)
|
||||||
|
const opts = {
|
||||||
|
useCORS: true, // 跨域图片
|
||||||
|
scale: 0.4,
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
html2canvas(document.getElementById('page-design-canvas'), opts).then(async (canvas) => {
|
||||||
|
cb(canvas.toDataURL('image/jpeg', 0.6))
|
||||||
|
gridSizeIndex = nowGrideSizeIndex
|
||||||
|
proxy?.updateZoom(nowZoom)
|
||||||
|
loading.close()
|
||||||
|
})
|
||||||
|
}, 301)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
save,
|
||||||
|
closePublish,
|
||||||
|
saveImg,
|
||||||
|
createImg,
|
||||||
|
...toRefs(state),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dZoom', 'dPage']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['selectWidget', 'updateZoom']),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
// Width variables (appears count calculates by raw css)
|
||||||
|
@width00: 400px; // Appears 2 times
|
||||||
|
@width20: 100%; // Appears 2 times
|
||||||
|
|
||||||
|
// Height variables (appears count calculates by raw css)
|
||||||
|
@height20: 400px; // Appears 2 times
|
||||||
|
|
||||||
|
.fill-info-wrap {
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
height: 100%;
|
||||||
|
padding: 50px;
|
||||||
|
position: absolute;
|
||||||
|
width: @width20;
|
||||||
|
z-index: 9999;
|
||||||
|
.fill-info-content {
|
||||||
|
background-color: @color-white;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-height: 861px;
|
||||||
|
min-height: 600px;
|
||||||
|
padding: 20px;
|
||||||
|
width: 600px;
|
||||||
|
.fill-info-step {
|
||||||
|
flex: 1;
|
||||||
|
width: @width20;
|
||||||
|
#cover-wrap {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: @height20;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: @width00;
|
||||||
|
#cover {
|
||||||
|
box-shadow: 1px 1px 10px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: @height20;
|
||||||
|
max-width: @width00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.publish-btn {
|
||||||
|
background-color: @color-main;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: @color-white;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
&:hover {
|
||||||
|
background-color: @color-dark-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.close-publish {
|
||||||
|
background-color: @color-white;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: @color-main;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin: 20px auto;
|
||||||
|
outline: 1px solid @color-main;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
&:hover {
|
||||||
|
background-color: @color-dark-gray;
|
||||||
|
color: @color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
40
src/components/common/PopoverTip.vue
Normal file
40
src/components/common/PopoverTip.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-04-10 12:12:57
|
||||||
|
* @Description: tooltip提示
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-10 12:42:02
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-popover ref="popover" :placement="position" :title="title" :width="width" trigger="hover" :content="content">
|
||||||
|
<template #reference>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
// top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end
|
||||||
|
position: {
|
||||||
|
default: 'bottom',
|
||||||
|
},
|
||||||
|
// offset: {
|
||||||
|
// default: 0,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
setup() {},
|
||||||
|
})
|
||||||
|
</script>
|
89
src/components/common/ProgressLoading/index.vue
Normal file
89
src/components/common/ProgressLoading/index.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-12-28 09:29:42
|
||||||
|
* @Description: 百分比进度条
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-13 23:05:29
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div v-if="percent" class="mask">
|
||||||
|
<div class="content">
|
||||||
|
<div class="text">{{ text }}</div>
|
||||||
|
<el-progress style="width: 100%" :text-inside="true" :percentage="percent" />
|
||||||
|
<div class="text btn" @click="cancel">{{ cancelText }}</div>
|
||||||
|
<div class="text info">{{ msg }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, watch } from 'vue'
|
||||||
|
import { ElProgress } from 'element-plus'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { ElProgress },
|
||||||
|
props: ['percent', 'text', 'cancelText', 'msg'],
|
||||||
|
emits: ['done', 'cancel'],
|
||||||
|
setup(props, context) {
|
||||||
|
watch(
|
||||||
|
() => props.percent,
|
||||||
|
(num) => {
|
||||||
|
if (num >= 100) {
|
||||||
|
setTimeout(() => {
|
||||||
|
context.emit('done')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
context.emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { cancel }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.el-progress-bar__innerText) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.mask {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 24%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 99999;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
margin: 2rem 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #3771e5;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
</style>
|
3
src/components/common/Uploader/index.ts
Normal file
3
src/components/common/Uploader/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import upload from './index.vue'
|
||||||
|
|
||||||
|
export default upload
|
130
src/components/common/Uploader/index.vue
Normal file
130
src/components/common/Uploader/index.vue
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-29 18:17:13
|
||||||
|
* @Description: 二次封装上传组件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-12 15:12:07
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-upload action="" accept="image/*" :http-request="upload" :show-file-list="false" multiple>
|
||||||
|
<slot>
|
||||||
|
<el-button size="small">上传图片<i class="el-icon-upload el-icon--right"></i></el-button>
|
||||||
|
</slot>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, nextTick } from 'vue'
|
||||||
|
import { ElUpload } from 'element-plus'
|
||||||
|
import Qiniu from '@/common/methods/QiNiu'
|
||||||
|
import { getImage } from '@/common/methods/getImgDetail'
|
||||||
|
import _config from '@/config'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { ElUpload },
|
||||||
|
props: {
|
||||||
|
modelValue: {},
|
||||||
|
options: {
|
||||||
|
default: () => {
|
||||||
|
return { bucket: 'cloud-design', prePath: 'user' }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hold: {
|
||||||
|
default: false, // 是否阻止上传操作,仅做文件选择
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['done', 'update:modelValue', 'load'],
|
||||||
|
setup(props, context) {
|
||||||
|
let uploading: boolean = false // 上传状态Flag
|
||||||
|
let timer: any = null
|
||||||
|
|
||||||
|
let uploadList: any[] = [] // 上传队列
|
||||||
|
let index: number = 0 // 当前上传的脚标
|
||||||
|
let count: number = 0 // 当前上传总数
|
||||||
|
|
||||||
|
let tempSimpleRes: any = null // 单个文件上传时返回
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
setTimeout(() => {
|
||||||
|
// 加载七牛上传插件
|
||||||
|
const link_element = document.createElement('script')
|
||||||
|
link_element.setAttribute('src', _config.QINIUYUN_PLUGIN)
|
||||||
|
document.head.appendChild(link_element)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
const upload = ({ file }: any) => {
|
||||||
|
if (props.hold) {
|
||||||
|
context.emit('load', file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadList.push(file)
|
||||||
|
clearTimeout(timer)
|
||||||
|
count++
|
||||||
|
updatePercent(null)
|
||||||
|
uploadQueue()
|
||||||
|
}
|
||||||
|
// 上传队列
|
||||||
|
const uploadQueue = async () => {
|
||||||
|
if (!uploading) {
|
||||||
|
uploading = true
|
||||||
|
if (uploadList[0]) {
|
||||||
|
tempSimpleRes = await qiNiuUpload(uploadList[0]) // 队列有文件,执行上传
|
||||||
|
const { width, height }: any = await getImage(uploadList[0])
|
||||||
|
context.emit('done', { width, height, url: _config.IMG_URL + tempSimpleRes.key }) // 单个文件进行响应
|
||||||
|
uploading = false
|
||||||
|
handleRemove() // 移除已上传文件
|
||||||
|
index++
|
||||||
|
updatePercent(null)
|
||||||
|
uploadQueue()
|
||||||
|
} else {
|
||||||
|
uploading = false
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
index = count = 0
|
||||||
|
updatePercent(0)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qiNiuUpload = async (file: File) => {
|
||||||
|
updatePercent(0)
|
||||||
|
return new Promise(async (resolve: Function) => {
|
||||||
|
if (props.hold) {
|
||||||
|
context.emit('load', file)
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
const result: any = await Qiniu.upload(file, props.options, (res: Type.Object) => {
|
||||||
|
updatePercent(res.total.percent)
|
||||||
|
})
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 更新视图
|
||||||
|
const updatePercent = (p?: number | null) => {
|
||||||
|
const num = typeof p === 'number' ? String(p) : p
|
||||||
|
const percent = { ...props.modelValue }
|
||||||
|
percent.num = num ? Number(num).toFixed(0) : percent.num
|
||||||
|
percent.ratio = count ? `${index} / ${count}` : ''
|
||||||
|
context.emit('update:modelValue', percent)
|
||||||
|
}
|
||||||
|
const handleRemove = () => {
|
||||||
|
if (uploadList.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadList.splice(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
upload,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
:deep(.el-upload) {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
30
src/components/modules/index.ts
Normal file
30
src/components/modules/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-07-13 22:51:29
|
||||||
|
* @Description: require.context自动引用
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2022-04-08 10:28:47
|
||||||
|
*/
|
||||||
|
function capitalizeFirstLetter(string: string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排除要全局引入的组件,可以是目录名也可以是文件名
|
||||||
|
const exclude = ['settings', 'layout']
|
||||||
|
|
||||||
|
const regex = RegExp('.*^(?!.*?(' + exclude.join('|') + ')).*\\.vue$')
|
||||||
|
|
||||||
|
// const requireComponent = require.context('.', true, /\.vue$/) // 找到components文件夹下以.vue命名的文件
|
||||||
|
const requireComponent = import.meta.globEager('./**/*.vue')
|
||||||
|
|
||||||
|
function guide(Vue: Type.Object) {
|
||||||
|
for (const fileName in requireComponent) {
|
||||||
|
if (regex.test(fileName)) {
|
||||||
|
const componentConfig = requireComponent[fileName]
|
||||||
|
const componentName = capitalizeFirstLetter(fileName.replace(/^\..*\//, '').replace(/\.\w+$/, ''))
|
||||||
|
Vue.component(componentName, componentConfig.default || componentConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default guide
|
300
src/components/modules/layout/designBoard.vue
Normal file
300
src/components/modules/layout/designBoard.vue
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div id="page-design" ref="page-design" :style="{ paddingTop: dPaddingTop + 'px' }">
|
||||||
|
<div
|
||||||
|
id="out-page"
|
||||||
|
class="out-page"
|
||||||
|
:style="{
|
||||||
|
width: (dPage.width * dZoom) / 100 + 120 + 'px',
|
||||||
|
height: (dPage.height * dZoom) / 100 + 120 + 'px',
|
||||||
|
opacity: 1 - (dZoom < 100 ? dPage.tag : 0),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<div
|
||||||
|
:id="pageDesignCanvasId"
|
||||||
|
class="design-canvas"
|
||||||
|
:data-type="dPage.type"
|
||||||
|
:data-uuid="dPage.uuid"
|
||||||
|
:style="{
|
||||||
|
width: dPage.width + 'px',
|
||||||
|
height: dPage.height + 'px',
|
||||||
|
transform: 'scale(' + dZoom / 100 + ')',
|
||||||
|
transformOrigin: (dZoom >= 100 ? 'center' : 'left') + ' top',
|
||||||
|
backgroundColor: dPage.backgroundColor,
|
||||||
|
backgroundImage: `url(${dPage?.backgroundImage})`,
|
||||||
|
backgroundSize: dPage?.backgroundTransform?.x ? 'auto' : 'cover',
|
||||||
|
backgroundPositionX: (dPage?.backgroundTransform?.x || 0) + 'px',
|
||||||
|
backgroundPositionY: (dPage?.backgroundTransform?.y || 0) + 'px',
|
||||||
|
opacity: dPage.opacity + (dZoom < 100 ? dPage.tag : 0),
|
||||||
|
}"
|
||||||
|
@mousemove="dropOver($event)"
|
||||||
|
@drop="drop($event)"
|
||||||
|
@mouseup="drop($event)"
|
||||||
|
>
|
||||||
|
<!-- <grid-size /> -->
|
||||||
|
|
||||||
|
<!-- :class="{
|
||||||
|
layer: true,
|
||||||
|
'layer-active': getIsActive(layer.uuid),
|
||||||
|
'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid,
|
||||||
|
}" -->
|
||||||
|
<component :is="layer.type" v-for="layer in getlayers()" :id="layer.uuid" :key="layer.uuid" :class="['layer', { 'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid, 'layer-no-hover': dActiveElement.uuid === layer.uuid }]" :data-title="layer.type" :params="layer" :parent="dPage" :data-type="layer.type" :data-uuid="layer.uuid">
|
||||||
|
<template v-if="layer.isContainer">
|
||||||
|
<!-- :class="{
|
||||||
|
layer: true,
|
||||||
|
'layer-active': getIsActive(widget.uuid),
|
||||||
|
'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent,
|
||||||
|
'layer-hover': widget.uuid === dHoverUuid,
|
||||||
|
}" -->
|
||||||
|
<component :is="widget.type" v-for="widget in getChilds(layer.uuid)" :key="widget.uuid" child :class="['layer', { 'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent }]" :data-title="widget.type" :params="widget" :parent="layer" :data-type="widget.type" :data-uuid="widget.uuid" />
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<!-- <ref-line v-if="dSelectWidgets.length === 0" /> -->
|
||||||
|
<!-- <size-control v-if="dSelectWidgets.length === 0" /> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, nextTick } from 'vue'
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import { getTarget } from '@/common/methods/target'
|
||||||
|
|
||||||
|
import setWidgetData from '@/common/methods/DesignFeatures/setWidgetData'
|
||||||
|
import PointImg from '@/utils/plugins/pointImg'
|
||||||
|
import getComponentsData from '@/common/methods/DesignFeatures/setComponents'
|
||||||
|
import { debounce } from 'throttle-debounce'
|
||||||
|
|
||||||
|
// 页面设计组件
|
||||||
|
const NAME = 'page-design'
|
||||||
|
|
||||||
|
import { move, moveInit } from '@/mixins/move'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: NAME,
|
||||||
|
// components: {lineGuides},
|
||||||
|
mixins: [moveInit],
|
||||||
|
props: ['pageDesignCanvasId'],
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dPaddingTop', 'dPage', 'dZoom', 'dScreen', 'dWidgets', 'dActiveElement', 'dHoverUuid', 'dSelectWidgets', 'dAltDown', 'dDraging', 'showRotatable']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getScreen()
|
||||||
|
document.getElementById('page-design').addEventListener('mousedown', this.handleSelection, false)
|
||||||
|
document.getElementById('page-design').addEventListener('mousemove', debounce(100, false, this.handleMouseMove), false)
|
||||||
|
},
|
||||||
|
beforeUnmount() {},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['updateScreen', 'selectWidget', 'deleteWidget', 'addWidget', 'addGroup']),
|
||||||
|
async dropOver(e) {
|
||||||
|
if (this.dActiveElement.editable || this.dActiveElement.lock) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
let { data, type } = this.$store.getters.selectItem
|
||||||
|
if (type !== 'image') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const target = await getTarget(e.target)
|
||||||
|
const uuid = target.getAttribute('data-uuid')
|
||||||
|
this.$store.dispatch('setDropOver', uuid)
|
||||||
|
if (e.target.getAttribute('putIn')) {
|
||||||
|
this._dropIn = uuid
|
||||||
|
const imgUrl = data.value.thumb || data.value.url
|
||||||
|
!this._srcCache && (this._srcCache = target.firstElementChild.firstElementChild.src)
|
||||||
|
target.firstElementChild.firstElementChild.src = imgUrl
|
||||||
|
} else {
|
||||||
|
this._srcCache && (target.firstElementChild.firstElementChild.src = this._srcCache)
|
||||||
|
this._srcCache = ''
|
||||||
|
this._dropIn = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async drop(e) {
|
||||||
|
if (!this.dDraging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$store.commit('setDraging', false)
|
||||||
|
const dropIn = this._dropIn
|
||||||
|
this._dropIn = ''
|
||||||
|
this.$store.dispatch('setDropOver', '-1')
|
||||||
|
this.$store.commit('setShowMoveable', false) // 清理上一次的选择
|
||||||
|
let lost = e.target.className !== 'design-canvas' // className === 'design-canvas' , id: "page-design-canvas"
|
||||||
|
// e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
let { data: item, type } = JSON.parse(JSON.stringify(this.$store.getters.selectItem))
|
||||||
|
// 清除临时数据
|
||||||
|
this.$store.commit('selectItem', {})
|
||||||
|
let setting = {}
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 处理数据
|
||||||
|
setting = await setWidgetData(type, item, setting)
|
||||||
|
// 绝对坐标
|
||||||
|
const lostX = e.x - document.getElementById('page-design-canvas').getBoundingClientRect().left
|
||||||
|
const lostY = e.y - document.getElementById('page-design-canvas').getBoundingClientRect().top
|
||||||
|
// 放置组合
|
||||||
|
if (type === 'group') {
|
||||||
|
let parent = {}
|
||||||
|
item = await getComponentsData(item)
|
||||||
|
item.forEach((element) => {
|
||||||
|
if (element.type === 'w-group') {
|
||||||
|
parent.width = element.width
|
||||||
|
parent.height = element.height
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const half = { x: parent.width ? (parent.width * this.$store.getters.dZoom) / 100 / 2 : 0, y: parent.height ? (parent.height * this.$store.getters.dZoom) / 100 / 2 : 0 }
|
||||||
|
item.forEach((element) => {
|
||||||
|
element.left += (lost ? lostX - half.x : e.layerX - half.x) * (100 / this.$store.getters.dZoom)
|
||||||
|
element.top += (lost ? lostY - half.y : e.layerY - half.y) * (100 / this.$store.getters.dZoom)
|
||||||
|
})
|
||||||
|
this.addGroup(item)
|
||||||
|
}
|
||||||
|
// 设置坐标
|
||||||
|
const half = { x: setting.width ? (setting.width * this.$store.getters.dZoom) / 100 / 2 : 0, y: setting.height ? (setting.height * this.$store.getters.dZoom) / 100 / 2 : 0 }
|
||||||
|
// const half = { x: (this.dDragInitData.offsetX * this.dZoom) / 100, y: (this.dDragInitData.offsetY * this.dZoom) / 100 }
|
||||||
|
setting.left = (lost ? lostX - half.x : e.layerX - half.x) * (100 / this.$store.getters.dZoom)
|
||||||
|
setting.top = (lost ? lostY - half.y : e.layerY - half.y) * (100 / this.$store.getters.dZoom)
|
||||||
|
if (lost && type === 'image') {
|
||||||
|
// 如果不从画布加入,且不是图片类型,则判断是否加入到svg中
|
||||||
|
const target = await getTarget(e.target)
|
||||||
|
const targetType = target.getAttribute('data-type')
|
||||||
|
const uuid = target.getAttribute('data-uuid')
|
||||||
|
if (targetType === 'w-mask') {
|
||||||
|
// 容器
|
||||||
|
this.$store.commit('setShowMoveable', true) // 恢复选择
|
||||||
|
const widget = this.dWidgets.find((item) => item.uuid === uuid)
|
||||||
|
widget.imgUrl = item.value.url
|
||||||
|
// if (e.target.className.baseVal) {
|
||||||
|
// !widget.imgs && (widget.imgs = {})
|
||||||
|
// widget.imgs[`${e.target.className.baseVal}`] = item.value.url
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
if (dropIn) {
|
||||||
|
const widget = this.dWidgets.find((item) => item.uuid == dropIn)
|
||||||
|
widget.imgUrl = item.value.url
|
||||||
|
console.log('加入+', widget)
|
||||||
|
this.$store.commit('setShowMoveable', true) // 恢复选择
|
||||||
|
} else {
|
||||||
|
this.addWidget(setting) // 正常加入面板
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === 'bg') {
|
||||||
|
console.log('背景图片放置')
|
||||||
|
} else if (type !== 'group') {
|
||||||
|
console.log(setting)
|
||||||
|
this.addWidget(setting) // 正常加入面板
|
||||||
|
}
|
||||||
|
// 清除临时数据
|
||||||
|
// this.$store.commit('selectItem', {})
|
||||||
|
},
|
||||||
|
getScreen() {
|
||||||
|
let screen = this.$refs['page-design']
|
||||||
|
this.updateScreen({
|
||||||
|
width: screen.offsetWidth,
|
||||||
|
height: screen.offsetHeight,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async handleMouseMove(e) {
|
||||||
|
const pImg = new PointImg(e.target)
|
||||||
|
const { rgba } = pImg.getColorXY(e.offsetX, e.offsetY)
|
||||||
|
if (rgba && rgba === 'rgba(0,0,0,0)') {
|
||||||
|
console.log('解析点位颜色: ', rgba)
|
||||||
|
let target = await getTarget(e.target)
|
||||||
|
target.style.pointerEvents = 'none'
|
||||||
|
setTimeout(() => {
|
||||||
|
target.style.pointerEvents = 'auto'
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleSelection(e) {
|
||||||
|
if (e.which === 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = await getTarget(e.target)
|
||||||
|
let type = target.getAttribute('data-type')
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
let uuid = target.getAttribute('data-uuid')
|
||||||
|
if (uuid !== '-1' && !this.dAltDown) {
|
||||||
|
let widget = this.dWidgets.find((item) => item.uuid === uuid)
|
||||||
|
if (widget.parent !== '-1' && widget.parent !== this.dActiveElement.uuid && widget.parent !== this.dActiveElement.parent) {
|
||||||
|
uuid = widget.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置选中元素
|
||||||
|
// this.$store.commit('setMoveable', false)
|
||||||
|
if (this.showRotatable !== false) {
|
||||||
|
this.selectWidget({
|
||||||
|
uuid: uuid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuid !== '-1') {
|
||||||
|
this.initmovement && this.initmovement(e) // 参见 mixins
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 取消选中元素
|
||||||
|
this.selectWidget({
|
||||||
|
uuid: '-1',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getlayers() {
|
||||||
|
return this.dWidgets.filter((item) => item.parent === this.dPage.uuid)
|
||||||
|
},
|
||||||
|
getChilds(uuid) {
|
||||||
|
return this.dWidgets.filter((item) => item.parent === uuid)
|
||||||
|
},
|
||||||
|
// getIsActive(uuid) {
|
||||||
|
// if (this.dSelectWidgets.length > 0) {
|
||||||
|
// let widget = this.dSelectWidgets.find((item) => item.uuid === uuid)
|
||||||
|
// if (widget) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
// } else {
|
||||||
|
// return uuid === this.dActiveElement.uuid
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#page-design {
|
||||||
|
height: 100%;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
.out-page {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px;
|
||||||
|
position: relative;
|
||||||
|
.design-canvas {
|
||||||
|
// transition: all 0.3s;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
box-shadow: 1px 1px 10px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
// z-index: -9999;
|
||||||
|
// overflow: hidden;
|
||||||
|
// overflow: auto;
|
||||||
|
}
|
||||||
|
// .design-canvas ::-webkit-scrollbar {
|
||||||
|
// display: none; /* Chrome Safari */
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
136
src/components/modules/layout/lineGuides.vue
Normal file
136
src/components/modules/layout/lineGuides.vue
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-04-08 10:31:34
|
||||||
|
* @Description:
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-06-29 18:07:40
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, watch } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import Guides from '@scena/guides'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: ['show'],
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore()
|
||||||
|
const container = 'page-design' // page-design out-page
|
||||||
|
let guidesTop: any = null
|
||||||
|
let guidesLeft: any = null
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(open) => {
|
||||||
|
open ? render() : destroy()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.getters.dZoom,
|
||||||
|
() => {
|
||||||
|
changeScroll()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// // let scrollX = 0
|
||||||
|
// // let scrollY = 0
|
||||||
|
// // window.addEventListener('resize', () => {
|
||||||
|
// // guides.resize()
|
||||||
|
// // })
|
||||||
|
// // window.addEventListener('wheel', (e) => {
|
||||||
|
// // scrollX += e.deltaX
|
||||||
|
// // scrollY += e.deltaY
|
||||||
|
// // guides.scrollGuides(scrollY)
|
||||||
|
// // guides.scroll(scrollX)
|
||||||
|
// // })
|
||||||
|
// })
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
guidesTop.destroy()
|
||||||
|
guidesLeft.destroy()
|
||||||
|
guidesTop = null
|
||||||
|
guidesLeft = null
|
||||||
|
}
|
||||||
|
function render() {
|
||||||
|
const sameParams: any = {
|
||||||
|
backgroundColor: '#f9f9fa',
|
||||||
|
lineColor: '#bec2c7',
|
||||||
|
textColor: '#999999',
|
||||||
|
// direction: 'start',
|
||||||
|
// height: 30,
|
||||||
|
displayDragPos: true,
|
||||||
|
dragPosFormat: (v: any) => v + 'px',
|
||||||
|
}
|
||||||
|
guidesTop = new Guides(document.getElementById(container), {
|
||||||
|
...sameParams,
|
||||||
|
type: 'horizontal',
|
||||||
|
className: 'my-horizontal',
|
||||||
|
}).on('changeGuides', (e) => {
|
||||||
|
console.log(e, e.guides)
|
||||||
|
// const el = document.getElementById('out-page')
|
||||||
|
// const top = 20 + (el?.offsetTop || 0)
|
||||||
|
// store.commit('updateGuidelines', { horizontalGuidelines: e.guides.map((x) => x + top) })
|
||||||
|
})
|
||||||
|
|
||||||
|
guidesLeft = new Guides(document.getElementById(container), {
|
||||||
|
...sameParams,
|
||||||
|
type: 'vertical',
|
||||||
|
className: 'my-vertical',
|
||||||
|
}).on('changeGuides', (e) => {
|
||||||
|
console.log(e, e.guides)
|
||||||
|
// store.commit('updateGuidelines', { verticalGuidelines: e.guides })
|
||||||
|
})
|
||||||
|
|
||||||
|
changeScroll()
|
||||||
|
}
|
||||||
|
function changeScroll() {
|
||||||
|
if (guidesTop && guidesLeft) {
|
||||||
|
const zoom = store.getters.dZoom / 100
|
||||||
|
guidesTop.zoom = zoom
|
||||||
|
guidesLeft.zoom = zoom
|
||||||
|
if (zoom < 0.9) {
|
||||||
|
guidesTop.unit = Math.floor(1 / zoom) * 50
|
||||||
|
guidesLeft.unit = Math.floor(1 / zoom) * 50
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('out-page')
|
||||||
|
const left = 60 + (el?.offsetLeft || 0)
|
||||||
|
const top = 30 + (el?.offsetTop || 0)
|
||||||
|
guidesTop.scroll(-left / zoom)
|
||||||
|
guidesTop.scrollGuides(-top / zoom)
|
||||||
|
guidesLeft.scroll(-top / zoom)
|
||||||
|
guidesLeft.scrollGuides(-(left - 30) / zoom)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
// :deep(.shortLineSize) {
|
||||||
|
// height: 1px !important;
|
||||||
|
// }
|
||||||
|
.my-horizontal,
|
||||||
|
.my-vertical {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
.my-horizontal {
|
||||||
|
left: 0px;
|
||||||
|
top: 0;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
height: 30px !important;
|
||||||
|
}
|
||||||
|
.my-vertical {
|
||||||
|
top: 30px;
|
||||||
|
left: 0px;
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
width: 30px !important;
|
||||||
|
}
|
||||||
|
</style>
|
181
src/components/modules/layout/sizeControl.vue
Normal file
181
src/components/modules/layout/sizeControl.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2021-08-03 17:50:21
|
||||||
|
* @Description: 旧大小控制组件,已交由moveable控制
|
||||||
|
* @LastEditors: ShawnPhang
|
||||||
|
* @LastEditTime: 2021-08-09 11:13:09
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div v-if="dActiveElement.record && dActiveElement.uuid !== '-1'" id="size-control">
|
||||||
|
<!-- 上左 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
cursor: 'nw-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'left-top')"
|
||||||
|
></div>
|
||||||
|
<!-- 上中 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'vertical' || dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + width / 2 + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
cursor: 'n-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'top')"
|
||||||
|
></div>
|
||||||
|
<!-- 上右 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + width + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
cursor: 'ne-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'right-top')"
|
||||||
|
></div>
|
||||||
|
<!-- 中左 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'horizontal' || dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + height / 2 + 'px',
|
||||||
|
cursor: 'w-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'left')"
|
||||||
|
></div>
|
||||||
|
<!-- 中右 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'horizontal' || dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + width + 'px',
|
||||||
|
top: top + height / 2 + 'px',
|
||||||
|
cursor: 'e-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'right')"
|
||||||
|
></div>
|
||||||
|
<!-- 下左 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + height + 'px',
|
||||||
|
cursor: 'sw-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'left-bottom')"
|
||||||
|
></div>
|
||||||
|
<!-- 下中 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'vertical' || dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + width / 2 + 'px',
|
||||||
|
top: top + height + 'px',
|
||||||
|
cursor: 's-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'bottom')"
|
||||||
|
></div>
|
||||||
|
<!-- 下右 -->
|
||||||
|
<div
|
||||||
|
v-if="dActiveElement.record.dir === 'all'"
|
||||||
|
class="square"
|
||||||
|
:style="{
|
||||||
|
left: left + width + 'px',
|
||||||
|
top: top + height + 'px',
|
||||||
|
cursor: 'se-resize',
|
||||||
|
}"
|
||||||
|
@mousedown="handlemousedown($event, 'right-bottom')"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
|
||||||
|
// 组件大小控制器
|
||||||
|
const NAME = 'size-control'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: NAME,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dirs: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dActiveElement', 'dWidgets']),
|
||||||
|
left() {
|
||||||
|
return parseInt(this.dActiveElement.left)
|
||||||
|
},
|
||||||
|
top() {
|
||||||
|
return parseInt(this.dActiveElement.top)
|
||||||
|
},
|
||||||
|
width() {
|
||||||
|
return parseInt(this.dActiveElement.record.width)
|
||||||
|
},
|
||||||
|
height() {
|
||||||
|
return parseInt(this.dActiveElement.record.height)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['dResize', 'initDResize', 'dResize', 'stopDResize']),
|
||||||
|
handlemousedown(e, dirs) {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.dirs = dirs.split('-')
|
||||||
|
this.initDResize({
|
||||||
|
startX: e.pageX,
|
||||||
|
startY: e.pageY,
|
||||||
|
originX: this.dActiveElement.left,
|
||||||
|
originY: this.dActiveElement.top,
|
||||||
|
width: this.width,
|
||||||
|
height: this.height,
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', this.handlemousemove, true)
|
||||||
|
document.addEventListener('mouseup', this.handlemouseup, true)
|
||||||
|
},
|
||||||
|
|
||||||
|
handlemousemove(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.dResize({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
dirs: this.dirs,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handlemouseup() {
|
||||||
|
document.removeEventListener('mousemove', this.handlemousemove, true)
|
||||||
|
document.removeEventListener('mouseup', this.handlemouseup, true)
|
||||||
|
this.stopDResize()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#size-control {
|
||||||
|
position: absolute;
|
||||||
|
.square {
|
||||||
|
background-color: #ffffff;
|
||||||
|
height: 10px;
|
||||||
|
outline: 1px solid #3b74f1;
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
width: 10px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
395
src/components/modules/layout/zoomControl.vue
Normal file
395
src/components/modules/layout/zoomControl.vue
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
<template>
|
||||||
|
<div id="zoom-control">
|
||||||
|
<ul v-show="show" class="zoom-selecter">
|
||||||
|
<li v-for="(item, index) in zoomList" :key="index" :class="['zoom-item', { 'zoom-item-active': activezoomIndex === index }]" @click.stop="selectItem(index)">
|
||||||
|
<!-- <i v-if="item.icon" :class="['iconfont', item.icon]"></i> -->
|
||||||
|
<span>{{ item.text }}</span>
|
||||||
|
<i v-if="activezoomIndex === index" class="iconfont icon-selected"></i>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="!hideControl" class="zoom-control-wrap">
|
||||||
|
<div :class="['zoom-icon radius-left', { disable: activezoomIndex === 0 }]" @click.stop="activezoomIndex > 0 ? sub() : ''">
|
||||||
|
<i class="iconfont icon-sub"></i>
|
||||||
|
</div>
|
||||||
|
<div :class="['zoom-text', { 'zoom-text-active': show }]" @click.stop="show = !show">{{ zoom.text }}</div>
|
||||||
|
<div :class="['zoom-icon radius-right', { disable: otherIndex === otherList.length - 1 }]" @click.stop="otherIndex < otherList.length - 1 ? add() : ''">
|
||||||
|
<i class="iconfont icon-add"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions } from 'vuex'
|
||||||
|
import addMouseWheel from '@/common/methods/addMouseWheel'
|
||||||
|
|
||||||
|
// 组件大小控制器
|
||||||
|
const NAME = 'zoom-control'
|
||||||
|
let holder = null
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: NAME,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hideControl: false,
|
||||||
|
activezoomIndex: 0,
|
||||||
|
zoomList: [
|
||||||
|
{
|
||||||
|
text: '25%',
|
||||||
|
value: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '50%',
|
||||||
|
value: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '75%',
|
||||||
|
value: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '100%',
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '125%',
|
||||||
|
value: 125,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '150%',
|
||||||
|
value: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '200%',
|
||||||
|
value: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '最佳尺寸',
|
||||||
|
value: -1,
|
||||||
|
// icon: 'icon-best-size',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
show: false,
|
||||||
|
zoom: {
|
||||||
|
value: 0,
|
||||||
|
text: 0,
|
||||||
|
},
|
||||||
|
otherList: [
|
||||||
|
{
|
||||||
|
text: '250%',
|
||||||
|
value: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '300%',
|
||||||
|
value: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '350%',
|
||||||
|
value: 350,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '400%',
|
||||||
|
value: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '450%',
|
||||||
|
value: 450,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '500%',
|
||||||
|
value: 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
otherIndex: -1,
|
||||||
|
bestZoom: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['dPage', 'dScreen', 'zoomScreenChange', 'dZoom']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activezoomIndex(value) {
|
||||||
|
if (value < 0 || value > this.zoomList.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.zoom = JSON.parse(JSON.stringify(this.zoomList[value]))
|
||||||
|
},
|
||||||
|
otherIndex(value) {
|
||||||
|
if (value < 0 || value > this.otherList.length - 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.zoom = JSON.parse(JSON.stringify(this.otherList[value]))
|
||||||
|
},
|
||||||
|
zoom(value) {
|
||||||
|
let realValue = value.value
|
||||||
|
if (realValue === -1) {
|
||||||
|
realValue = this.calcZoom()
|
||||||
|
}
|
||||||
|
this.updateZoom(realValue)
|
||||||
|
this.autoFixTop()
|
||||||
|
},
|
||||||
|
dScreen: {
|
||||||
|
handler() {
|
||||||
|
this.screenChange()
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
zoomScreenChange() {
|
||||||
|
this.activezoomIndex = this.zoomList.length - 1
|
||||||
|
this.screenChange()
|
||||||
|
},
|
||||||
|
dPage: {
|
||||||
|
handler(val) {
|
||||||
|
this.screenChange()
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.$nextTick()
|
||||||
|
window.addEventListener('click', this.close)
|
||||||
|
if (this.$route.path === '/draw') {
|
||||||
|
this.activezoomIndex = 3
|
||||||
|
this.hideControl = true
|
||||||
|
} else {
|
||||||
|
this.activezoomIndex = this.zoomList.length - 1
|
||||||
|
}
|
||||||
|
// 添加滚轮监听
|
||||||
|
addMouseWheel('page-design', (isDown) => {
|
||||||
|
this.mousewheelZoom(isDown)
|
||||||
|
})
|
||||||
|
// 添加窗口大小监听
|
||||||
|
window.addEventListener('resize', (event) => {
|
||||||
|
this.changeScreen()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('click', this.close)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['updateZoom', 'updateScreen']),
|
||||||
|
changeScreen() {
|
||||||
|
clearTimeout(holder)
|
||||||
|
holder = setTimeout(() => {
|
||||||
|
const screen = document.getElementById('page-design')
|
||||||
|
this.updateScreen({
|
||||||
|
width: screen.offsetWidth,
|
||||||
|
height: screen.offsetHeight,
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
screenChange() {
|
||||||
|
// 弹性尺寸即时修改
|
||||||
|
if (this.activezoomIndex === this.zoomList.length - 1) {
|
||||||
|
this.updateZoom(this.calcZoom())
|
||||||
|
this.autoFixTop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectItem(index) {
|
||||||
|
this.activezoomIndex = index
|
||||||
|
this.otherIndex = -1
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
close(e) {
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
add() {
|
||||||
|
this.curAction = 'add'
|
||||||
|
this.show = false
|
||||||
|
if (this.activezoomIndex === this.zoomList.length - 2 || this.activezoomIndex === this.zoomList.length - 1) {
|
||||||
|
this.activezoomIndex = this.zoomList.length
|
||||||
|
// this.otherIndex += 1
|
||||||
|
if (this.bestZoom) {
|
||||||
|
this.nearZoom(true)
|
||||||
|
} else {
|
||||||
|
this.otherIndex += 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.activezoomIndex != this.zoomList.length) {
|
||||||
|
this.activezoomIndex++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.otherIndex < this.otherList.length - 1) {
|
||||||
|
this.otherIndex++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sub() {
|
||||||
|
this.curAction = null
|
||||||
|
this.show = false
|
||||||
|
if (this.otherIndex === 0) {
|
||||||
|
this.otherIndex = -1
|
||||||
|
this.activezoomIndex = this.zoomList.length - 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.otherIndex != -1) {
|
||||||
|
this.otherIndex--
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.activezoomIndex === this.zoomList.length - 1) {
|
||||||
|
if (this.bestZoom) {
|
||||||
|
this.nearZoom()
|
||||||
|
} else {
|
||||||
|
this.activezoomIndex = this.zoomList.length - 2
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.activezoomIndex != 0) {
|
||||||
|
this.activezoomIndex--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mousewheelZoom(down) {
|
||||||
|
const value = Number(this.dZoom.toFixed(0))
|
||||||
|
this.updateZoom(down ? value - 1 : value + 1)
|
||||||
|
this.zoom.text = value + '%'
|
||||||
|
this.autoFixTop()
|
||||||
|
},
|
||||||
|
nearZoom(add) {
|
||||||
|
for (let i = 0; i < this.zoomList.length; i++) {
|
||||||
|
this.activezoomIndex = i
|
||||||
|
if (this.zoomList[i].value > this.bestZoom) {
|
||||||
|
if (add) break
|
||||||
|
} else if (this.zoomList[i].value < this.bestZoom) {
|
||||||
|
if (!add) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.bestZoom = 0
|
||||||
|
},
|
||||||
|
calcZoom() {
|
||||||
|
let widthZoom = ((this.dScreen.width - 142) * 100) / this.dPage.width
|
||||||
|
let heightZoom = ((this.dScreen.height - 122) * 100) / this.dPage.height
|
||||||
|
|
||||||
|
this.bestZoom = Math.min(widthZoom, heightZoom)
|
||||||
|
return this.bestZoom
|
||||||
|
},
|
||||||
|
async autoFixTop() {
|
||||||
|
await this.$nextTick()
|
||||||
|
const presetPadding = 60
|
||||||
|
const el = document.getElementById('out-page')
|
||||||
|
// const clientHeight = document.body.clientHeight - 54
|
||||||
|
const parentHeight = el.offsetParent.offsetHeight - 54
|
||||||
|
let padding = (parentHeight - el.offsetHeight) / 2
|
||||||
|
if (typeof this.curAction === 'undefined') {
|
||||||
|
padding += presetPadding / 2
|
||||||
|
}
|
||||||
|
this.curAction === 'add' && (padding -= presetPadding)
|
||||||
|
this.$store.commit('updatePaddingTop', padding > 0 ? padding : 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@color-select: #1b1634;
|
||||||
|
@color1: #ffffff; // 选项板背景
|
||||||
|
@color2: #ffffff; // Appears 3 times
|
||||||
|
@color3: #666666; // 文字主颜色
|
||||||
|
@color4: #c2c2c2; // 禁用
|
||||||
|
@color5: rgba(0, 0, 0, 0.12); // 高亮选项背景
|
||||||
|
@z-border-color: #e6e6e6;
|
||||||
|
|
||||||
|
#zoom-control {
|
||||||
|
bottom: 20px;
|
||||||
|
position: absolute;
|
||||||
|
right: 302px;
|
||||||
|
z-index: 1000;
|
||||||
|
.zoom-control-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
.radius-left {
|
||||||
|
border-bottom-left-radius: 50%;
|
||||||
|
border-top-left-radius: 50%;
|
||||||
|
border-block-end: 1px solid @z-border-color;
|
||||||
|
border-block-start: 1px solid @z-border-color;
|
||||||
|
}
|
||||||
|
.radius-right {
|
||||||
|
border-bottom-right-radius: 50%;
|
||||||
|
border-top-right-radius: 50%;
|
||||||
|
border-block-end: 1px solid @z-border-color;
|
||||||
|
border-block-start: 1px solid @z-border-color;
|
||||||
|
}
|
||||||
|
.zoom-icon {
|
||||||
|
align-items: center;
|
||||||
|
background-color: @color2;
|
||||||
|
color: @color3;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
&:hover {
|
||||||
|
background-color: @color1;
|
||||||
|
color: @color-select;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disable {
|
||||||
|
color: @color4;
|
||||||
|
&:hover {
|
||||||
|
background-color: @color2;
|
||||||
|
color: @color4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.zoom-text {
|
||||||
|
user-select: none;
|
||||||
|
align-items: center;
|
||||||
|
background-color: @color2;
|
||||||
|
color: @color3;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60px;
|
||||||
|
border-block-end: 1px solid @z-border-color;
|
||||||
|
border-block-start: 1px solid @z-border-color;
|
||||||
|
&:hover {
|
||||||
|
background-color: @color1;
|
||||||
|
color: @color-select;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.zoom-selecter {
|
||||||
|
background-color: @color1;
|
||||||
|
color: @color3;
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
&:after {
|
||||||
|
bottom: -8px;
|
||||||
|
content: '';
|
||||||
|
left: 50%;
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.zoom-item {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
i {
|
||||||
|
margin-right: 10px;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: @color5;
|
||||||
|
color: @color-select;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #zoom-control-active {
|
||||||
|
// background-color: @color1;
|
||||||
|
// background-color: @color5;
|
||||||
|
// color: @color-select;
|
||||||
|
// color: @color-select;
|
||||||
|
// }
|
||||||
|
</style>
|
265
src/components/modules/panel/components/layerList.vue
Normal file
265
src/components/modules/panel/components/layerList.vue
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<!--
|
||||||
|
* @Author: ShawnPhang
|
||||||
|
* @Date: 2022-03-07 17:25:19
|
||||||
|
* @Description: 图层组件
|
||||||
|
* @LastEditors: ShawnPhang <site: book.palxp.com>
|
||||||
|
* @LastEditTime: 2023-07-10 15:38:30
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<ul class="widget-list">
|
||||||
|
<!-- <li v-for="widget in getWidgets" :key="widget.uuid" :class="['widget', { active: getIsActive(widget.uuid) }, 'item-one']" @click="selectLayer(widget)" @mouseover="hoverLayer(widget.uuid)" @mouseout="hoverLayer('-1')">
|
||||||
|
<span v-show="widget.parent !== '-1'" :class="['widget-type icon', `sd-xiaji`]"></span>
|
||||||
|
<img v-if="widget.imgUrl" class="widget-type widget-type__img" :src="widget.imgUrl" />
|
||||||
|
<span v-else :class="['widget-type icon', `sd-${widget.type}`, widget.type]"></span>
|
||||||
|
<span :class="['widget-name', 'line-clamp-1', `${widget.type}`]">{{ widget.text || widget.name }}</span>
|
||||||
|
<div class="widget-out" :data-type="widget.type" :data-uuid="widget.uuid"></div>
|
||||||
|
</li> -->
|
||||||
|
<draggable v-model="widgets" group="type" item-key="uuid" v-bind="dragOptions" :move="onMove" @start="drag = true" @end="onDone">
|
||||||
|
<template #item="{ element }">
|
||||||
|
<li :class="['widget', { active: getIsActive(element.uuid), disable: !showItem(element) }, 'item-one']" @click="selectLayer(element)" @mouseover="hoverLayer(element)" @mouseout="hoverLayer('-1')">
|
||||||
|
<!-- <span v-show="+element.parent !== -1" :class="['widget-type icon', `sd-xiaji`]"></span> -->
|
||||||
|
<span v-show="+element.parent !== -1" class="second-layer"></span>
|
||||||
|
<img v-if="element.imgUrl" class="widget-type widget-type__img" :src="element.imgUrl" />
|
||||||
|
<img v-else-if="element.svgUrl" class="widget-type widget-type__img" :src="element.svgUrl" />
|
||||||
|
<span v-else :class="['widget-type icon', `sd-${element.type}`, element.type]"></span>
|
||||||
|
<span :class="['widget-name', 'line-clamp-1', `${element.type}`]">{{ element.text || element.name }} {{ element.mask ? '(容器)' : '' }}</span>
|
||||||
|
<div class="widget-out" :data-type="element.type" :data-uuid="element.uuid">
|
||||||
|
<i :class="['icon', element.lock ? 'sd-suoding' : 'sd-jiesuo']" @click.stop="lockLayer(element)" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
|
||||||
|
<!-- <li :class="['widget', { active: dActiveElement.uuid === dPage.uuid && dSelectWidgets.length === 0 }]" @click="selectLayer(dPage)" @mouseover="hoverLayer('-1')" @mouseout="hoverLayer('-1')">
|
||||||
|
<span class="widget-type"></span>
|
||||||
|
<span class="widget-name">{{ dPage.name }}</span>
|
||||||
|
<div class="widget-out" :data-type="dPage.type" :data-uuid="dPage.uuid"></div>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed, reactive, ref, toRefs } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { draggable },
|
||||||
|
props: ['data'],
|
||||||
|
emits: ['change'],
|
||||||
|
setup(props, context) {
|
||||||
|
let widgets: any = ref([])
|
||||||
|
const state: any = reactive({
|
||||||
|
drag: false,
|
||||||
|
})
|
||||||
|
const dragOptions = computed(() => {
|
||||||
|
return {
|
||||||
|
animation: 300,
|
||||||
|
// disabled: !state.editable,
|
||||||
|
ghostClass: 'ghost',
|
||||||
|
chosenClass: 'choose',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
// const dPage = computed(() => {
|
||||||
|
// return store.getters.dPage
|
||||||
|
// })
|
||||||
|
// const dActiveElement = computed(() => {
|
||||||
|
// return store.getters.dActiveElement
|
||||||
|
// })
|
||||||
|
// const dSelectWidgets = computed(() => {
|
||||||
|
// return store.getters.dSelectWidgets
|
||||||
|
// })
|
||||||
|
const showItem = (item: any) => {
|
||||||
|
return state.drag === true && item.parent != '-1' ? false : true
|
||||||
|
}
|
||||||
|
// const cWidgets = computed(() => {
|
||||||
|
// return getWidgets()
|
||||||
|
// })
|
||||||
|
const getWidgets = () => {
|
||||||
|
let widgets = []
|
||||||
|
let len = props.data.length
|
||||||
|
const data = props.data.slice(0)
|
||||||
|
const childs = [] // 临时子组件
|
||||||
|
for (let i = len - 1; i >= 0; --i) {
|
||||||
|
let widget = JSON.parse(JSON.stringify(data[i]))
|
||||||
|
if (widget.parent != -1) {
|
||||||
|
childs.unshift(widget)
|
||||||
|
} else {
|
||||||
|
widgets.push(widget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item of childs) {
|
||||||
|
// widgets[widgets.findIndex((x) => x.uuid === item.parent)].childs.push(item)
|
||||||
|
const index = widgets.findIndex((x) => x.uuid === item.parent)
|
||||||
|
widgets.splice(index + 1, 0, item)
|
||||||
|
}
|
||||||
|
return widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIsActive = (uuid: number) => {
|
||||||
|
if (store.getters.dSelectWidgets.length > 0) {
|
||||||
|
let widget = store.getters.dSelectWidgets.find((item: any) => item.uuid === uuid)
|
||||||
|
if (widget) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return uuid === store.getters.dActiveElement.uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectLayer = (widget: any) => {
|
||||||
|
// console.log(widget)
|
||||||
|
store.dispatch('selectWidget', { uuid: widget.uuid })
|
||||||
|
}
|
||||||
|
const hoverLayer = ({ uuid, parent }: any) => {
|
||||||
|
store.dispatch('updateHoverUuid', uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMove = ({ relatedContext, draggedContext }: any) => {
|
||||||
|
const relatedElement = relatedContext.element
|
||||||
|
const draggedElement = draggedContext.element
|
||||||
|
// const index = props.data.findIndex((x: any) => x.uuid === draggedElement.uuid)
|
||||||
|
// const toIndex = props.data.findIndex((x: any) => x.uuid === relatedElement.uuid)
|
||||||
|
// console.log(index, toIndex)
|
||||||
|
return (!relatedElement || relatedElement.parent == -1) && draggedElement.parent == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDone = () => {
|
||||||
|
state.drag = false
|
||||||
|
context.emit('change', widgets.value)
|
||||||
|
}
|
||||||
|
// 锁定图层
|
||||||
|
const lockLayer = (item: any) => {
|
||||||
|
store.dispatch('updateWidgetData', {
|
||||||
|
uuid: item.uuid,
|
||||||
|
key: 'lock',
|
||||||
|
value: typeof item.lock === 'undefined' ? true : !item.lock,
|
||||||
|
pushHistory: false,
|
||||||
|
})
|
||||||
|
// item.lock = typeof item.lock === 'undefined' ? true : !item.lock
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lockLayer, onDone, onMove, selectLayer, hoverLayer, widgets, getWidgets, getIsActive, ...toRefs(state), dragOptions, showItem }
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
async handler(nval) {
|
||||||
|
this.widgets = this.getWidgets()
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@color0: #ffffff; // Appears 5 times
|
||||||
|
@color1: #999999; // Appears 3 times
|
||||||
|
@color2: rgba(0, 0, 0, 0.05); // Appears 2 times
|
||||||
|
|
||||||
|
.widget-list {
|
||||||
|
width: 100%;
|
||||||
|
.widget {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid @color2;
|
||||||
|
color: @color1;
|
||||||
|
// cursor: move;
|
||||||
|
cursor: grab;
|
||||||
|
display: flex;
|
||||||
|
padding: 8px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
.widget-type {
|
||||||
|
// outline: 1px solid #dedede;
|
||||||
|
align-items: center;
|
||||||
|
color: @color1;
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
&__img {
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: @color0;
|
||||||
|
background-image: -webkit-linear-gradient(45deg, #efefef 25%, transparent 25%, transparent 75%, #efefef 75%, #efefef), -webkit-linear-gradient(45deg, #efefef 25%, transparent 25%, transparent 75%, #efefef 75%, #efefef);
|
||||||
|
background-position: 0 0, 10px 10px;
|
||||||
|
background-size: 21px 21px;
|
||||||
|
outline: 1px solid #dedede;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widget-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-right: 22px;
|
||||||
|
}
|
||||||
|
.widget-out {
|
||||||
|
height: 100%;
|
||||||
|
margin-left: -12px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.widget-out:hover > .sd-jiesuo {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widget.active {
|
||||||
|
background-color: #888888;
|
||||||
|
color: @color0;
|
||||||
|
}
|
||||||
|
.item-one {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
// .item-two {
|
||||||
|
// padding-left: 40px;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-group {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
// icons
|
||||||
|
.sd-jiesuo,
|
||||||
|
.sd-suoding {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: default;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
.sd-jiesuo {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.sd-xiaji {
|
||||||
|
margin: 0 -4px 0 32px !important;
|
||||||
|
}
|
||||||
|
.second-layer {
|
||||||
|
margin-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dragable
|
||||||
|
.choose {
|
||||||
|
border: 1px dashed #999999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
.disable {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: @main-color;
|
||||||
|
}
|
||||||
|
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user