Merge pull request #53 from JeremyYu-cn/feat-upgrade-vue3

Feat upgrade vue3
This commit is contained in:
Shawn Phang 2024-02-25 23:18:29 +08:00 committed by GitHub
commit a25caeb762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 39471 additions and 1191 deletions

2
.gitignore vendored
View File

@ -2,7 +2,6 @@
node_modules
/dist
config.json
package-lock.json
screenshot/node_modules/
screenshot/dist/
@ -16,6 +15,7 @@ screenshot/_apidoc/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock*
pnpm-debug.log*
# Editor directories and files

View File

@ -1,7 +1,7 @@
{
"files.eol": "\n",
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
"editor.codeActionsOnSave": {

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 ShawnPhang
Copyright (c) 2023-present ShawnPhang (design.palxp.cn)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

105
README.md
View File

@ -1,4 +1,4 @@
**[在线体验](https://design.palxp.cn/)** | **[中文文档网站](https://xp.palxp.cn/)** | [架构及目录说明](https://xp.palxp.cn/#/articles/1689321259854)
**[在线体验网址](https://design.palxp.cn/)** | **[中文文档](https://xp.palxp.cn/)** | [常见问题](https://xp.palxp.cn/#/articles/1689323321667) | [架构说明](https://xp.palxp.cn/#/articles/1689321259854)
---
@ -8,17 +8,17 @@
适用于海报图片生成、电商分享图、文章长图、视频/公众号封面等多种场景。
![](https://xp.palxp.cn/images/2023-7-16-1689500112694.gif)
[![](https://xp.palxp.cn/images/2023-7-16-1689500112694.gif)](https://design.palxp.cn/)
- 丝滑的操作体验,丰富的交互细节,基础功能完善
- 采用服务端生成图片,确保多端出图统一性,支持各种 CSS 特性
- 丝滑的页面操作体验,丰富的交互细节,基础功能完善
- 采用服务端生成图片,确保多端出图统一性,支持各种 CSS 特性
- 简易 AI 抠图工具,上传图片一键去除背景
- 技术栈Vue3 、Vite2 、Vuex 、ElementPlus
- 技术栈Vue3 、Vite2 、Vuex 、ElementPlus,开发体验畅快
- 图片生成Puppeteer、Express
### 支持功能
- 导入 PSD 文件解析成模板、在线导出图片下载
- 导入 PSD 文件解析成模板、在线导出图片下载
- 元素拖拽、组合、缩放、层级调整、对齐等操作。
- 图片素材插入、替换、裁剪,图片容器等功能。
- SVG 素材颜色、透明度编辑,文字花字组合。
@ -29,98 +29,69 @@
- 图层操作,支持拖拽变更层级。
- 颜色调色板原生级取色器颜色吸管Chrome
### 拉取源码
> 环境需求:**Node.js v16.18** 以上版本
## 快速开始
```
git clone https://github.com/palxiao/poster-design.git
cd poster-design
```
### 安装依赖
```
npm run prepared
```
> 网络太慢尝试运行npm config set registry https://registry.npmmirror.com 再安装依赖
### 本地运行
```
npm run serve
```
> 将会同时运行前端界面与图片生成服务(`3000`端口为前端项目,`7001`端口为图片生成服务)
>
> ![](https://xp.palxp.cn/images/2023-7-16-1689498291322.png)
>
> 如果你本地没有成功启动两个服务,可能是 win 系统不兼容,手动进 `screenshot` 目录安装依赖(`npm install`)并启动服务(`npm run dev`) 或者使用 VSCode 自带的终端来运行命令,注意不要使用 CMD。
![](https://xp.palxp.cn/images/2023-7-16-1689498291322.png)
### 打包
访问 http://127.0.0.1:3000/ 查看网页。点此查看[完整说明文档](https://xp.palxp.cn/#/articles/1689319644311)。
| 前端页面 | 截图服务 |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `npm run v-build` | [/screenshot/README.md](https://github.com/palxiao/poster-design/blob/main/screenshot/README.md) |
### 图片生成服务
### 截图服务
代码位于根目录 [/screenshot](https://github.com/palxiao/poster-design/tree/main/screenshot)接口API文档点此查看[接口 API 文档](https://xp.palxp.cn/apidoc/screenshot.html)。
代码位于 [/screenshot](https://github.com/palxiao/poster-design/tree/main/screenshot) 目录下,查看[接口 API 文档](https://xp.palxp.cn/apidoc/screenshot.html)。
> 打包注意事项与服务器配置相关请进入该目录下查看 README 文件说明。
### 截图服务 Docker 部署
可以通过 Docker 运行一个带 Linux 浏览器的容器,[参考说明](https://xp.palxp.cn/#/articles/1689319644311?id=docker%e5%ae%b9%e5%99%a8)。
> 更多相关事项请进入该目录下查看 [README.md](https://github.com/palxiao/poster-design/blob/main/screenshot/README.md) 文件。 Docker 部署:[参考说明](https://xp.palxp.cn/#/articles/1689319644311?id=docker%e5%ae%b9%e5%99%a8)。
### 服务端
根据你的具体场景自行实现,目前本项目中的所有后端接口参考:[接口 API 文档](https://xp.palxp.cn/apidoc/index.html)。
目前本项目演示 Demo 中的后端接口参考:[接口 API 文档](https://xp.palxp.cn/apidoc/index.html)。
### 抠图服务部署
## 其它
```
docker run -d -p 5000:5000 --restart always danielgatis/rembg s
```
项目最早使用 Vue2 开发,后改用 Vue3 重构,目前有部分代码混合了 Options 写法。
目前也还在持续迭代中,有很多的不足,我也是一边学习一边成长。[实时迭代计划文档](https://xp.palxp.cn/#/articles/1689319986889?id=%e8%bf%ad%e4%bb%a3%e8%ae%a1%e5%88%92).
### 感谢
项目还使用或参考了一些优秀开源项目,包括但不限于:
- [moveable](https://github.com/daybrush/moveable): 提供了画布中选择、拖动缩放等能力
- [html2canvas](https://github.com/niklasvh/html2canvas): 前端生成图片兜底方案
- [html2canvas](https://github.com/niklasvh/html2canvas): 前端生图的一种快捷方案
- [qr-code-styling](https://qr-code-styling.com/): 风格化二维码
- [rembg](https://github.com/danielgatis/rembg): 图片删除背景,使用 u2net 预训练模型
- [rembg](https://github.com/danielgatis/rembg): 图片抠图,使用 u2net 预训练模型
### Q&A
或许你在工作中有类似的需求,或许你对开发编辑器感兴趣,也希望这个项目能给到你一些微薄帮助!
Q**项目可以直接部署上线吗?**
### `Star`
A本项目支持本地运行体验完整功能如需部署到生产需自行开发配套的后端接口自行部署图片生成服务部署方法参考项目中的文档。
Q**后端源码不开源吗?**
A考虑到服务端的开发语言、数据库类型都可能不尽相同且本项目中实现简单代码不具备参考性所以暂时只提供接口 API 文档。(目前仅是一些增删改查)
### 其它
项目最早使用 Vue2 开发,后改用 Vue3 重构,所以有部分代码混合了 Options 写法。
或许你在工作中有类似的需求,或许你也对开发编辑器感兴趣,希望这个项目能给到你一些微薄帮助!
目前本项目也还在迭代中,有很多的不足,我也是一边学习一边成长。开源不易,希望看到这里的你可以给本项目点个 **Star** 支持一下~
开源不易,别忘了给本项目点个 **Star** ~
[![Star History Chart](https://api.star-history.com/svg?repos=palxiao/poster-design&type=Date)](https://star-history.com/#palxiao/poster-design&Date)
#### 部分迭代计划:
感谢所有支持本项目的朋友 :heart:
- [ ] P1: 文字特效属性编辑面板开发
- [ ] P1字体抽取功能
- [ ] P1PSD 解析重构(涉及基础库更换)
[![Stargazers](https://bytecrank.com/nastyox/reporoster/php/stargazersSVG.php?user=palxiao&repo=poster-design)](https://github.com/palxiao/poster-design/stargazers)
[ -> 完整后续迭代计划文档](https://xp.palxp.cn/#/articles/1689319986889?id=%e8%bf%ad%e4%bb%a3%e8%ae%a1%e5%88%92)
### `Fork`
### LICENSE
这些小伙伴都在使用迅排设计 :heart:
[![Forkers](https://bytecrank.com/nastyox/reporoster/php/forkersSVG.php?user=palxiao&repo=poster-design)](https://github.com/palxiao/poster-design/network/members)
### `Contributions`
<a href="https://github.com/palxiao/poster-design/graphs/contributors">
<img src="https://contrib.rocks/image?repo=palxiao/poster-design" />
</a>
### `LICENSE`
本项目完全免费,遵循 [MIT 开源许可证](https://github.com/palxiao/poster-design/blob/main/LICENSE)
[MIT License](https://github.com/palxiao/poster-design/blob/main/LICENSE)

View File

@ -3,7 +3,7 @@
* @Date: 2022-01-17 16:06:30
* @Description: Design Palxp
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-13 13:12:09
* @LastEditTime: 2023-09-19 18:36:13
-->
<!DOCTYPE html>
<html lang="zh-CN">
@ -18,6 +18,6 @@
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script defer src="/snap.svg-min.js"></script>
<script defer src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
</body>
</html>

37684
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,8 @@
"publish-fast": "git add . && git commit -m 'build: auto publish' && sh script/publish.sh"
},
"dependencies": {
"@gradio/client": "^0.1.4",
"@palxp/color-picker": "^1.3.1",
"@palxp/color-picker": "^1.5.5",
"@palxp/image-extraction": "^1.2.4",
"@scena/guides": "^0.18.1",
"@webtoon/psd": "^0.4.0",
"axios": "^0.21.1",
@ -31,8 +31,7 @@
"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": "3.2.22",
"vue-router": "^4.0.0-0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0-0"
@ -42,7 +41,7 @@
"@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",
"@vitejs/plugin-vue": "1.9.3",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
@ -58,13 +57,13 @@
"eslint-config-alloy": "~4.1.0",
"eslint-plugin-vue": "^7.12.1",
"less": "^4.1.1",
"typescript": "~4.1.5",
"typescript": "^4.4.3",
"unplugin-element-plus": "^0.7.1",
"vite": "^2.4.1",
"vite": "2.6.4",
"vite-plugin-compression": "^0.3.0",
"vue-cli-plugin-norm": "~1.2.2",
"vue-eslint-parser": "^7.6.0",
"vue-tsc": "^0.2.0"
"vue-tsc": "0.3.0"
},
"browserslist": [
"> 1%",

View File

@ -9,7 +9,7 @@ module.exports = {
'ie >= 8',
'last 10 versions', // 所有主流浏览器最近10版本用
],
grid: true,
grid: false,
},
},
}

View File

@ -7,8 +7,15 @@
"author": "ShawnPhang",
"license": "ISC",
"dependencies": {
"axios": "^0.24.0",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"fontmin": "^1.0.1",
"form-data": "^4.0.0",
"moment": "^2.18.1",
"multiparty": "^4.2.3",
"mysql": "^2.13.0",
"qiniu": "^7.4.0",
"puppeteer": "^10.4.0",
"images": "3.2.3"
},

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2020-07-22 20:13:14
* @Description:
* @LastEditors: ShawnPhang <site: m.palxp.cn>
* @LastEditTime: 2023-09-04 14:08:10
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-16 10:03:51
*/
const { saveScreenshot } = require('../utils/download-single.ts')
const uuid = require('../utils/uuid.ts')
@ -48,6 +48,7 @@ module.exports = {
*
* @apiParam {String|Number} id () id
* @apiParam {String|Number} tempid () idid时取该值
* @apiParam {String|Number} tempType ()
* @apiParam {String} width ()
* @apiParam {String} height ()
* @apiParam {String} screenshot_url
@ -55,19 +56,20 @@ module.exports = {
* @apiParam {String} size ,
* @apiParam {String} quality ,
*/
let { id, tempid, width, height, screenshot_url, type = 'file', size, quality } = req.query
let { id, tempid, tempType, width, height, screenshot_url, type = 'file', size, quality } = req.query
const url = (screenshot_url || drawLink) + `${id ? '?id=' : '?tempid='}`
id = id || tempid
const path = filePath + `${id}-screenshot.png`
const thumbPath = type === 'cover' ? filePath + `${id}-cover.jpg` : null
const thumbPath = type === 'cover' && tempType != 1 ? 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 })
const targetUrl = url + id + `${tempType?'&tempType='+tempType:''}`
// console.log(targetUrl, path, thumbPath);
queueRun(saveScreenshot, targetUrl, { width, height, path, thumbPath, size, quality })
.then(() => {
res.setHeader('Content-Type', 'image/jpg')
// const stats = fs.statSync(path)

View File

@ -3,7 +3,7 @@
* @Date: 2021-09-30 14:47:22
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-18 18:17:54
* @LastEditTime: 2023-10-16 10:56:35
*/
const isDev = process.env.NODE_ENV === 'development'
const puppeteer = require('puppeteer')
@ -16,16 +16,28 @@ const maxPXs = 4211840 // 超出此规格会触发限制器降低dpr节省服
const maximum = 5000 // 最大宽高限制,超过截断以防止服务崩溃
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) => {
return new Promise(async (resolve: Function, reject: Function) => {
let isPageLoad = false
let browser: any = null
// 格式化浏览器宽高
width = Number(width).toFixed(0)
height = Number(height).toFixed(0)
// 启动浏览器
let browser = await puppeteer.launch({
headless: true, // !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,
})
try {
browser = await puppeteer.launch({
headless: true, // !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,
})
} catch (error) {
console.log('Puppeteer启动错误', '窗口大小:', width, height);
}
if (!browser) {
reject()
return false
}
const regulators = setTimeout(() => {
browser && browser.close()
browser = null

View File

@ -3,12 +3,12 @@
* @Date: 2023-09-18 17:34:44
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-18 18:05:22
* @LastEditTime: 2023-09-20 16:10:58
-->
<template>
<div id="appindex">
<div class="viewWrap">
<router-view></router-view>
<router-view />
</div>
</div>
</template>
@ -17,21 +17,8 @@
#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>

24
src/api/ai.ts Normal file
View File

@ -0,0 +1,24 @@
/*
* @Author: ShawnPhang
* @Date: 2021-08-27 14:42:15
* @Description: AI相关接口
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-13 00:07:19
*/
import fetch from '@/utils/axios'
// 上传接口
export const upload = (file: File, cb: Function) => {
const formData = new FormData()
formData.append('file', file)
const extra = {
responseType: 'blob',
onUploadProgress: (progress: any) => {
cb(Math.floor((progress.loaded / progress.total) * 100), 0)
},
onDownloadProgress: (progress: any) => {
cb(100, Math.floor((progress.loaded / progress.total) * 100))
},
}
return fetch('https://res.palxp.cn/ai/upload', formData, 'post', {}, extra)
}

View File

@ -2,13 +2,15 @@
* @Author: ShawnPhang
* @Date: 2021-08-19 18:43:22
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-21 13:06:46
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-28 15:46:56
*/
import * as home from './home'
import * as material from './material'
import * as ai from './ai'
export default {
home,
material,
ai,
}

View File

@ -1,9 +1,9 @@
/*
* @Author: ShawnPhang
* @Date: 2021-08-27 14:42:15
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-21 11:19:04
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 11:40:47
*/
import fetch from '@/utils/axios'
@ -15,7 +15,7 @@ export const getList = (params: Type.Object = {}) => fetch('design/material', pa
// 获取字体
export const getFonts = (params: Type.Object = {}) => fetch('design/fonts', params)
export const getFontSub = (params: Type.Object = {}) => fetch('design/font_sub', params)
export const getFontSub = (params: Type.Object = {}, extra: any = {}) => fetch('design/font_sub', params, 'get', {}, extra)
// 图库列表
export const getImagesList = (params: Type.Object = {}) => fetch('design/imgs', params, 'get')
@ -23,6 +23,7 @@ export const getImagesList = (params: Type.Object = {}) => fetch('design/imgs',
// 我的上传列表
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 deleteMyWorks = (params: Type.Object = {}) => fetch('design/poster/del', params, 'post')
// 添加图片
export const addMyPhoto = (params: Type.Object = {}) => fetch('design/user/add_image', params)

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-02-12 11:08:57
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-12 11:09:41
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:35:44
*/
export default [
{

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-07-17 11:20:22
* @Description:
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:15:14
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-24 17:07:44
*/
export default [
{
@ -31,14 +31,14 @@ export default [
show: false,
component: 'photo-list-wrap',
},
// {
// name: '背景',
// icon: 'icon-beijing',
// show: false,
// component: 'bg-img-list-wrap',
// },
{
name: '背景',
icon: 'icon-beijing',
show: false,
component: 'bg-img-list-wrap',
},
{
name: '工具',
name: '工具箱',
icon: 'icon-zujian01',
show: false,
component: 'tools-list-wrap',

View File

@ -13,7 +13,7 @@
@height2: 54px; // Appears 5 times
.page-design-bg-color {
background-color: #4b678c;
// background-color: #4b678c;
// background-color: #4682b4;
}
#page-design-index {
@ -132,16 +132,7 @@
pointer-events: none;
}
.shelter {
box-shadow: 0 0 0 5000px rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 5000px rgba(248, 248, 248, 0.99);
z-index: 8;
}
.shelter-bg {
// background-color: #ffffff;
background-color: #f0f0f0;
background-image: linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);
background-position: 0 0, 8px 8px;
background-size: 16px 16px;
overflow: hidden;
user-select: none;
}
}

View File

@ -25,3 +25,12 @@
.line-clamp-2 {
-webkit-line-clamp: 2;
}
.transparent-bg {
background-color: #f0f0f0;
background-image: linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);
background-position: 0 0, 8px 8px;
background-size: 16px 16px;
overflow: hidden;
user-select: none;
}

View File

@ -1,3 +1,5 @@
// Some primary CSS, and reset CSS
@font-face {
font-family: TitleFont;
src: url(../fonts/xpsj.subset.woff2) format('woff2');
@ -15,10 +17,14 @@ body {
-webkit-font-smoothing: antialiased;
color: #333333;
font-family: Hiragino Sans GB, Hiragino Sans GB W3, Arial, Microsoft Yahei, STHeiti, sans-serif;
overflow: hidden;
}
* {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
// font-size: 14px;
scrollbar-width: none; /* 火狐滚动条无法自定义宽度,只能通过此属性使滚动条宽度变细 */
-ms-overflow-style: none; /* 隐藏滚动条在IE和Edge两个浏览器中很难更改样式固采取隐藏方式 */
@ -56,7 +62,9 @@ select:-webkit-autofill:focus {
}
a {
text-decoration: none;
// text-decoration: none;
color: inherit;
text-decoration: inherit;
}
ul,
ol,
@ -73,10 +81,28 @@ h6 {
li {
list-style: none;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
vertical-align: middle;
}
img {
-webkit-user-drag: none;
user-select: none;
}
img,
video {
max-width: 100%;
height: auto;
}
input {
outline: 0;
&:-moz-focusring {
@ -105,5 +131,5 @@ p {
pointer-events: none;
}
.hide {
opacity: 0;
opacity: 0 !important;
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2023-07-10 14:58:48
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-11 11:10:03
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-22 18:11:15
*/
import store from '@/store'
export default class dragHelper {
@ -18,6 +18,8 @@ export default class dragHelper {
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))
} else {
this.finish()
}
})
// 鼠标抬起
@ -91,6 +93,9 @@ export default class dragHelper {
}
// 结束/完成处理(动画)
private finish(done = false) {
if (!this.dragging) {
return
}
this.dragging = false
store.commit('setDraging', false)
store.commit('selectItem', {})

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-02-22 15:06:14
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-10 17:37:27
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-11 17:36:44
*/
import store from '@/store'
import { getImage } from '../getImgDetail'
@ -29,6 +29,5 @@ export default async function setItem2Data(item: any) {
cloneItem.canvasWidth = cloneItem.width * (store.getters.dZoom / 100)
// cloneItem.canvasHeight = cloneItem.height * (store.getters.dZoom / 100)
return cloneItem
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-29 20:35:31
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-11 22:02:12
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 16:11:55
*/
import dayjs from 'dayjs'
import api from '@/api/album'
@ -18,7 +18,7 @@ export default {
upload: async (file: File, options: Options, cb?: Function) => {
const win: any = window
let name = ''
const suffix = file.type.split('/')[1] // 文件后缀
const suffix = file.type.split('/')[1] || 'png' // 文件后缀
if (!options.fullPath) {
// const DT: any = await exifGetTime(file) // 照片时间
const DT: any = new Date()

View File

@ -2,12 +2,13 @@
* @Author: ShawnPhang
* @Date: 2022-01-08 09:43:37
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-25 11:13:01
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-13 01:30:33
*/
// import { isSupportFontFamily, blob2Base64 } from './utils'
import { getFonts } from '@/api/material'
// import store from '@/store'
const nowVersion = '2' // 当前字体文件版本更新,将刷新前端缓存
const fontList: any = []
// const download: any = {}
@ -16,7 +17,7 @@ export const useFontStore = {
// download,
async init() {
this.list = []
localStorage.getItem('FONTS_VERSION') !== '1' && localStorage.removeItem('FONTS')
localStorage.getItem('FONTS_VERSION') !== nowVersion && localStorage.removeItem('FONTS')
const localFonts: any = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
if (localFonts.length > 0) {
this.list.push(...localFonts)
@ -26,12 +27,12 @@ export const useFontStore = {
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 }
const { id, alias, oid, value, preview, woff, lang } = x
return { id, oid, value, preview, alias, url: woff, lang }
}),
)
localStorage.setItem('FONTS', JSON.stringify(this.list))
localStorage.setItem('FONTS_VERSION', '1')
localStorage.setItem('FONTS_VERSION', nowVersion)
}
// store.dispatch('setFonts', this.list)
},

View File

@ -77,7 +77,7 @@ export function filterSkyFonts() {
// );
const textClouds: any = []
;((textClouds as unknown) as CloudText[]).forEach((cloud) => {
;(textClouds as unknown as CloudText[]).forEach((cloud) => {
// 找到文字组件字体
if (cloud.fontFamily && !fonts.includes(cloud.fontFamily)) {
fonts.push(cloud.fontFamily)
@ -114,6 +114,10 @@ export function base642Blob(b64Data: string, contentType = '', sliceSize = 512)
export async function blob2Base64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
if (blob.type === 'application/json') {
resolve('')
return
}
const fileReader = new FileReader()
fileReader.onload = () => {
resolve(fileReader.result as string)

View File

@ -1,9 +1,9 @@
/*
* @Author: ShawnPhang
* @Date: 2021-08-23 17:25:35
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-24 00:16:59
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 10:42:54
*/
export const getImage = (imgItem: string | File) => {
// 创建对象
@ -19,7 +19,7 @@ export const getImage = (imgItem: string | File) => {
resolve(img)
} else {
// 加载完成执行
img.onload = function() {
img.onload = function () {
resolve(img)
}
}

View File

@ -3,21 +3,25 @@
* @Date: 2023-07-11 23:50:22
* @Description: 抠图组件
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-15 12:38:49
* @LastEditTime: 2023-10-09 00:42:48
-->
<template>
<el-dialog v-model="show" title="AI抠图(测试版)" width="650" @close="handleClose">
<el-dialog v-model="show" title="AI 智能抠图" align-center 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>
<div class="el-upload__tip">服务器带宽过低为了更好的体验请上传 2M 内的图片</div>
</uploader>
<el-progress v-if="!cutImage && progressText" :percentage="progress">
<el-button text>
{{ progressText }} <span v-show="progress">{{ progress }}%</span>
</el-button>
</el-progress>
<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>
<div v-show="rawImage" v-loading="!cutImage" :style="{ width: offsetWidth ? offsetWidth + 'px' : '100%' }" class="scan-effect transparent-bg">
<img ref="raw" :style="{ 'clip-path': 'inset(0 0 0 ' + percent + '%)' }" :src="rawImage" alt="" />
<img v-show="cutImage" :src="cutImage" alt="结果图像" @mousemove="mousemove" />
<div v-show="cutImage" :style="{ left: percent + '%' }" class="scan-line"></div>
</div>
@ -25,23 +29,33 @@
<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>
<el-button v-show="rawImage && toolModel" @click="clear">清空重选</el-button>
<el-button v-show="cutImage" type="primary" plain @click="edit">进入编辑模式</el-button>
<el-button v-show="cutImage && toolModel" type="primary" plain @click="download"> 下载 </el-button>
<el-button v-show="cutImage && !toolModel" v-loading="loading" type="primary" plain @click="cutDone"> {{ loading ? '上传中..' : '完成抠图' }} </el-button>
</span>
</template>
<ImageExtraction ref="matting" />
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, nextTick } from 'vue'
import { defineComponent, reactive, toRefs, nextTick } from 'vue'
import { useStore } from 'vuex'
import { ElProgress } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import uploader from '@/components/common/Uploader/index.vue'
import _dl from '@/common/methods/download'
import api from '@/api'
import Qiniu from '@/common/methods/QiNiu'
import _config from '@/config'
import { getImage } from '@/common/methods/getImgDetail'
import ImageExtraction from './ImageExtraction.vue'
export default defineComponent({
components: { uploader, UploadFilled },
setup() {
components: { uploader, UploadFilled, ElProgress, ImageExtraction },
emits: ['done'],
setup(props, { emit }) {
const store = useStore()
const state: any = reactive({
show: false,
@ -50,18 +64,18 @@ export default defineComponent({
raw: null,
offsetWidth: 0,
percent: 0,
progress: 0,
progressText: '',
toolModel: true,
loading: false,
matting: null,
})
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('上传小于 2M 的图片')
alert('上传图片超出限制')
return false
}
//
@ -70,15 +84,33 @@ export default defineComponent({
})
state.rawImage = URL.createObjectURL(file)
fileName = file.name
//
const result = await app.predict('/predict', [file, 'u2netp', ''])
state.rawImage && (state.cutImage = result?.data[0])
requestAnimationFrame(run)
//
const result: any = await api.ai.upload(file, (up: number, dp: number) => {
if (dp) {
state.progressText = dp === 100 ? '' : '导入中..'
state.progress = dp
} else {
state.progressText = up < 100 ? '上传中..' : '正在处理,请稍候..'
state.progress = up < 100 ? up : 0
}
})
if (result.type !== 'application/json') {
const resultImage = URL.createObjectURL(result)
state.rawImage && (state.cutImage = resultImage)
requestAnimationFrame(run)
} else alert('服务器繁忙,请稍等下重新尝试~')
}
const open = () => {
const open = (file: File) => {
state.loading = false
state.show = true
store.commit('setShowMoveable', false)
nextTick(() => {
if (file) {
selectFile(file)
state.toolModel = false
}
})
}
const handleClose = () => {
@ -108,6 +140,30 @@ export default defineComponent({
state.percent < 100 ? requestAnimationFrame(run) : (isRuning = false)
}
const cutDone = async () => {
state.loading = true
const response = await fetch(state.cutImage)
const buffer = await response.arrayBuffer()
const file = new File([buffer], `cut_image_${Math.random()}.png`)
// upload
const qnOptions = { bucket: 'xp-design', prePath: 'user' }
const result = await Qiniu.upload(file, qnOptions)
const { width, height } = await getImage(file)
const url = _config.IMG_URL + result.key
await api.material.addMyPhoto({ width, height, url })
emit('done', url)
state.show = false
handleClose()
}
const edit = () => {
state.matting.open(state.rawImage, state.cutImage, (base64: any) => {
state.cutImage = base64
state.percent = 0
requestAnimationFrame(run)
})
}
return {
clear,
download,
@ -116,6 +172,8 @@ export default defineComponent({
open,
handleClose,
...toRefs(state),
cutDone,
edit,
}
},
})
@ -146,12 +204,6 @@ export default defineComponent({
object-fit: contain;
position: absolute;
}
.bg {
width: 100%;
height: 100%;
background: #ffffff;
position: absolute;
}
}
.scan-line {
@ -163,4 +215,8 @@ export default defineComponent({
// 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);
}
.progress {
width: 100%;
}
</style>

View File

@ -0,0 +1,106 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-10-08 14:15:17
* @Description: 手动抠图 - 修补擦除
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 01:28:11
-->
<template>
<div>
<el-dialog v-model="show" align-center width="90%" @close="showMatting = false">
<template #header>
<div class="tool-wrap">
<el-button type="primary" plain @click="done">确认应用</el-button>
<el-radio-group v-model="isErasing" style="margin-left: 35px">
<el-radio :label="false" size="large"> <b>修补画笔</b> <i class="icon sd-xiubu" /></el-radio>
<el-radio :label="true" size="large"> <b>擦除画笔</b> <i class="icon sd-cachu" /></el-radio>
</el-radio-group>
<number-slider v-model="radius" class="slider-wrap" label="画笔尺寸" :showInput="false" labelWidth="90px" :maxValue="constants.RADIUS_SLIDER_MAX" :minValue="constants.RADIUS_SLIDER_MIN" :step="constants.RADIUS_SLIDER_STEP" />
<number-slider v-model="hardness" class="slider-wrap" label="画笔硬度" :showInput="false" labelWidth="90px" :maxValue="constants.HARDNESS_SLIDER_MAX" :minValue="constants.HARDNESS_SLIDER_MIN" :step="constants.HARDNESS_SLIDER_STEP" />
</div>
</template>
<matting v-if="showMatting" :hasHeader="false" @register="mattingStart" />
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, nextTick } from 'vue'
import matting, { MattingType } from '@palxp/image-extraction'
import { ElRadioGroup, ElRadio } from 'element-plus'
import numberSlider from '@/components/modules/settings/numberSlider.vue'
export default defineComponent({
components: { matting, ElRadioGroup, ElRadio, numberSlider },
setup() {
const state: any = reactive({
show: false,
showMatting: false,
isErasing: false,
radius: 0, //
brushSize: '', //
hardness: 0, //
hardnessText: '', //
constants: {},
})
const params: any = { raw: '', result: '' }
let matting: MattingType | any = {}
let callback: any = null //
const mattingStart: any = (mattingOptions: MattingType) => {
mattingOptions.initLoadImages(params.raw, params.result)
state.isErasing = mattingOptions.isErasing
state.radius = mattingOptions.radius
state.hardness = mattingOptions.hardness
state.constants = mattingOptions.constants
matting = mattingOptions
}
const open = async (raw: any, result: any, cb: any) => {
state.show = true
params.raw = raw
params.result = result
await nextTick()
setTimeout(() => {
state.showMatting = true
}, 300)
callback = cb
}
const done = () => {
state.show = false
callback(matting.getResult())
}
return {
...toRefs(state),
open,
done,
mattingStart,
}
},
})
</script>
<style lang="less" scoped>
:deep(.el-dialog__body) {
padding: 0 !important;
}
:deep(.el-dialog__header) {
padding: 10px 35px;
// var(--el-dialog-padding-primary)
}
.tool-wrap {
display: flex;
align-items: center;
}
// .tool-left {
// display: inline-flex;
// flex: 1;
// }
.slider-wrap {
margin-left: 35px;
width: 240px;
}
</style>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-04 11:46:39
* @Description: 原版movable插件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-17 11:53:30
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-14 11:41:23
-->
<template>
<div id="empty" class="moveable__remove-item zk-moveable-style"></div>
@ -40,9 +40,9 @@ export default defineComponent({
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
case 'w-svg':
this.moveable.renderDirections = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se']
break
default:
this.moveable.renderDirections = ['nw', 'ne', 'sw', 'se']
break
@ -227,12 +227,12 @@ export default defineComponent({
this.updateWidgetData({
uuid: this.dActiveElement.uuid,
key: 'left',
value: this.holdPosition?.left,
value: Number(this.holdPosition?.left),
})
this.updateWidgetData({
uuid: this.dActiveElement.uuid,
key: 'top',
value: this.holdPosition?.top,
value: Number(this.holdPosition?.top),
})
this.holdPosition = null // important
setTimeout(() => {

View File

@ -2,18 +2,22 @@
* @Author: ShawnPhang
* @Date: 2022-10-08 10:07:19
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-29 17:57:46
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 00:04:51
-->
<template>
<el-dialog v-model="dialogVisible" title="选择图片" @close="close">
<el-tabs tab-position="left" style="height: 60vh" class="demo-tabs">
<el-tab-pane label="个人素材">
<el-tabs tab-position="left" style="height: 60vh" class="demo-tabs" @tab-change="tabChange">
<el-tab-pane label="我的素材">
<div class="pic__box">
<photo-list ref="imgListComp" :isDone="isDone" :listData="imgList" @load="load" @select="selectImg" />
<photo-list :isDone="isDone" :listData="imgList" @load="load" @select="selectImg" />
</div>
</el-tab-pane>
<el-tab-pane label="照片图库">
<div class="pic__box">
<photo-list :isDone="isPicsDone" :listData="recommendImgList" @load="loadPic" @select="selectImg($event, recommendImgList)" />
</div>
</el-tab-pane>
<el-tab-pane label="照片库"></el-tab-pane>
</el-tabs>
<!-- <template #footer>
<span class="dialog-footer">
@ -40,12 +44,14 @@ export default defineComponent({
const state: any = reactive({
dialogVisible: false,
imgList: [],
recommendImgList: [],
isDone: false,
isPicsDone: false,
})
let loading = false
let page = 0
let listPage = 0
let picPage = 0
const load = (init?: boolean) => {
if (init) {
state.imgList = []
@ -64,6 +70,22 @@ export default defineComponent({
}, 100)
})
}
const loadPic = (init?: boolean) => {
if (state.isPicsDone || loading) {
return
}
if (init && state.recommendImgList.length > 0) {
return
}
loading = true
picPage += 1
api.material.getImagesList({ page: picPage }).then(({ list }: any) => {
list.length <= 0 ? (state.isPicsDone = true) : (state.recommendImgList = state.recommendImgList.concat(list))
setTimeout(() => {
loading = false
}, 100)
})
}
const open = () => {
state.dialogVisible = true
@ -75,18 +97,26 @@ export default defineComponent({
store.commit('setShowMoveable', true)
}
const selectImg = (index: number) => {
const item: any = state.imgList[index]
const selectImg = (index: number, list: any) => {
const item: any = list ? list[index] : state.imgList[index]
context.emit('select', item)
state.dialogVisible = false
}
const tabChange = (index: any) => {
if (index == 1) {
loadPic(true)
}
}
return {
...toRefs(state),
open,
close,
load,
loadPic,
selectImg,
tabChange,
}
},
})

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-03-16 09:15:52
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-04-08 17:53:00
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-31 15:43:10
-->
<template>
<div ref="qrCodeDom" class="qrcode__wrap"></div>
@ -46,7 +46,7 @@ export default defineComponent({
type: 'canvas' as DrawType, // canvas svg
data: props.value,
image: props.image, // /favicon.svg
margin: 2,
margin: 0,
qrOptions: {
typeNumber: 3 as TypeNumber,
mode: 'Byte' as Mode,

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-29 18:17:13
* @Description: 二次封装上传组件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-12 15:12:07
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 15:46:02
-->
<template>
<el-upload action="" accept="image/*" :http-request="upload" :show-file-list="false" multiple>
@ -75,7 +75,7 @@ export default defineComponent({
if (file.size <= 1024 * 1024) {
tempSimpleRes = await qiNiuUpload(file) //
const { width, height }: any = await getImage(file)
useNotification('上传成功', '目前没有用户系统,请注意别上传隐私照片哦!', { position: 'bottom-left' })
useNotification('上传成功', '公共测试账户,上传请注意保护隐私哦!', { position: 'bottom-left' })
context.emit('done', { width, height, url: _config.IMG_URL + tempSimpleRes.key }) //
} else useNotification('爱护小水管', '请上传小于 1M 的图片哦!', { type: 'error', position: 'bottom-left' })
uploading = false

View File

@ -91,6 +91,10 @@ export default defineComponent({
beforeUnmount() {},
methods: {
...mapActions(['updateScreen', 'selectWidget', 'deleteWidget', 'addWidget', 'addGroup']),
// getBackground(data) {
// if (data.startsWith('http')) return `url(${data})`
// if (data.startsWith('linear-gradient')) return data
// },
async dropOver(e) {
if (this.dActiveElement.editable || this.dActiveElement.lock) {
return false

View File

@ -19,15 +19,17 @@
</div>
</template>
<script>
<script lang="ts">
import { mapGetters, mapActions } from 'vuex'
import { defineComponent } from 'vue'
import addMouseWheel from '@/common/methods/addMouseWheel'
//
const NAME = 'zoom-control'
let holder = null
let holder: number | undefined
export default {
// TODO: TS
export default defineComponent({
name: NAME,
data() {
return {
@ -101,6 +103,7 @@ export default {
],
otherIndex: -1,
bestZoom: 0,
curAction: "",
}
},
computed: {
@ -154,7 +157,7 @@ export default {
this.activezoomIndex = this.zoomList.length - 1
}
//
addMouseWheel('page-design', (isDown) => {
addMouseWheel('page-design', (isDown: boolean) => {
this.mousewheelZoom(isDown)
})
//
@ -171,6 +174,7 @@ export default {
clearTimeout(holder)
holder = setTimeout(() => {
const screen = document.getElementById('page-design')
if (!screen) return
this.updateScreen({
width: screen.offsetWidth,
height: screen.offsetHeight,
@ -184,12 +188,12 @@ export default {
this.autoFixTop()
}
},
selectItem(index) {
selectItem(index: number) {
this.activezoomIndex = index
this.otherIndex = -1
this.show = false
},
close(e) {
close(_: MouseEvent) {
this.show = false
},
add() {
@ -214,7 +218,7 @@ export default {
}
},
sub() {
this.curAction = null
this.curAction = null as any
this.show = false
if (this.otherIndex === 0) {
this.otherIndex = -1
@ -237,13 +241,14 @@ export default {
this.activezoomIndex--
}
},
mousewheelZoom(down) {
mousewheelZoom(down: boolean) {
const value = Number(this.dZoom.toFixed(0))
if (down && value <= 1) return
this.updateZoom(down ? value - 1 : value + 1)
this.zoom.text = value + '%'
this.zoom.text = (value + '%') as any
this.autoFixTop()
},
nearZoom(add) {
nearZoom(add?: boolean) {
for (let i = 0; i < this.zoomList.length; i++) {
this.activezoomIndex = i
if (this.zoomList[i].value > this.bestZoom) {
@ -265,8 +270,10 @@ export default {
await this.$nextTick()
const presetPadding = 60
const el = document.getElementById('out-page')
if (!el) return
// const clientHeight = document.body.clientHeight - 54
const parentHeight = el.offsetParent.offsetHeight - 54
const parentHeight = (el.offsetParent as HTMLElement).offsetHeight - 54
let padding = (parentHeight - el.offsetHeight) / 2
if (typeof this.curAction === 'undefined') {
padding += presetPadding / 2
@ -275,7 +282,7 @@ export default {
this.$store.commit('updatePaddingTop', padding > 0 ? padding : 0)
},
},
}
})
</script>
<style lang="less" scoped>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-03-07 17:25:19
* @Description: 图层组件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-10 15:38:30
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-24 11:39:02
-->
<template>
<ul class="widget-list">

View File

@ -3,7 +3,7 @@
<div class="widget-classify">
<ul class="classify-wrap">
<li v-for="(item, index) in widgetClassifyList" :key="index" :class="['classify-item', { 'active-classify-item': activeWidgetClassify === index }]" @click="clickClassify(index)">
<i :class="['iconfont', 'icon', item.icon]" :style="item.style"></i>
<div class="icon-box"><i :class="['iconfont', 'icon', item.icon]" :style="item.style" /></div>
<p>{{ item.name }}</p>
</li>
</ul>
@ -16,9 +16,9 @@
</div>
<!-- <div v-show="active" class="side-wrap"><div class="pack__up" @click="active = false">&lt;</div></div> -->
<div v-show="active" class="side-wrap">
<el-tooltip effect="dark" content="收起侧边栏" placement="right">
<!-- <el-tooltip effect="dark" content="收起侧边栏" placement="right"> -->
<div class="pack__up" @click="active = false"></div>
</el-tooltip>
<!-- </el-tooltip> -->
</div>
</div>
</template>
@ -26,7 +26,7 @@
<script lang="ts">
//
const NAME = 'widget-panel'
import widgetClassifyListData from '@/assets/data/widgetClassifyList'
import widgetClassifyListData from '@/assets/data/WidgetClassifyList.ts'
import { reactive, toRefs, onMounted, watch, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { mapActions } from 'vuex'
import { useRoute } from 'vue-router'
@ -49,7 +49,7 @@ export default {
onMounted(async () => {
await nextTick()
const { koutu } = route.query
koutu && (state.activeWidgetClassify = 5)
koutu && (state.activeWidgetClassify = 4)
})
watch(
@ -125,9 +125,17 @@ export default {
justify-content: center;
width: 100%;
p {
color: #9da3ac;
color: #666666;
font-weight: 600;
margin-top: 2px;
}
.icon-box {
width: 24px;
height: 27px;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
color: #070707;
}

View File

@ -2,20 +2,20 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 15:16:07
* @Description: 背景图
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:18:54
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-24 16:39:27
-->
<template>
<div class="wrap">
<div class="color__box">
<div class="color__box" :style="modelStyle.color">
<div v-for="c in colors" :key="c" :style="{ background: c }" class="color__item" @click="setBGcolor(c)"></div>
</div>
<ul v-if="showList" v-infinite-scroll="loadData" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<el-space fill wrap :fillRatio="30" direction="horizontal" class="list">
<div v-for="(item, i) in bgList" :key="i + 'i'" draggable="false" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img" :src="item.thumb" fit="cover"></el-image>
</div>
</el-space>
<div class="list" :style="modelStyle.list">
<imageTip v-for="(item, i) in bgList" :key="i + 'i'" :detail="item">
<el-image class="list__img" :src="item.thumb" fit="cover" lazy loading="lazy" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)"></el-image>
</imageTip>
</div>
<div v-show="loading" class="loading"><i class="el-icon-loading"></i> 拼命加载中</div>
<div v-show="loadDone" class="loading">全部加载完毕</div>
</ul>
@ -23,13 +23,16 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, watch } from 'vue'
// import { ElDivider } from 'element-plus'
import { defineComponent, reactive, toRefs, computed } from 'vue'
import api from '@/api'
import { mapActions, useStore } from 'vuex'
export default defineComponent({
// components: { ElDivider },
props: {
model: {
default: 'widgetPanel'
}
},
setup(props) {
const store = useStore()
const state = reactive({
@ -39,6 +42,19 @@ export default defineComponent({
showList: true,
colors: ['#000000ff', '#999999ff', '#CCCCCCff', '#FFFFFFff', '#E65353ff', '#FFD835ff', '#70BC59ff', '#607AF4ff', '#976BEEff'],
})
//
const models: any = {
widgetPanel: {
color: 'padding: 1.2rem 1rem',
list: 'grid-template-columns: auto auto auto;padding: 0 1rem;',
},
stylePanel: {
color: 'padding: 1.2rem 0;',
list: 'grid-template-columns: repeat(3, 76px);',
}
}
const modelStyle = computed(() => models[props.model])
const pageOptions = { page: 0, pageSize: 20 }
const loadData = () => {
@ -93,6 +109,7 @@ export default defineComponent({
load,
setBGcolor,
loadData,
modelStyle
}
},
methods: {
@ -137,11 +154,13 @@ export default defineComponent({
// }
.list {
width: 100%;
padding: 0 0 0 1rem;
display: grid;
grid-gap: 5px;
&__img {
cursor: pointer;
width: 92px;
width: 100%;
height: 92px;
border-radius: 4px;
}
&__img:hover::before {
content: ' ';
@ -165,7 +184,6 @@ export default defineComponent({
.color {
&__box {
padding: 1.2rem 1rem;
display: flex;
flex-wrap: wrap;
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 15:16:07
* @Description: 素材列表主要用于文字组合列表
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:14:27
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-11 17:59:57
-->
<template>
<div class="wrap">
@ -15,27 +15,22 @@
</el-input>
</div>
<el-divider content-position="left">推荐组件</el-divider> -->
<div v-show="!currentCategory" class="content__wrap">
<div v-for="(t, ti) in types" :key="ti + 't'">
<div v-if="showList[ti]?.length > 0" class="types__header" @click="selectTypes(t)">
<span style="flex: 1">{{ t.name }}</span>
<span class="types__header-more">全部<i class="iconfont icon-right"></i></span>
</div>
<div v-else class="loading"><i class="el-icon-loading"></i> 拼命加载中</div>
<classHeader v-show="!currentCategory" :types="types" @select="selectTypes">
<template v-slot="{ index }">
<div class="list-wrap">
<div v-for="(item, i) in showList[ti]" :key="i + 'sl'" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img-thumb" :src="item.cover" fit="contain"></el-image>
<div v-for="(item, i) in showList[index]" :key="i + 'sl'" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img-thumb" :src="item.cover" fit="contain" lazy loading="lazy"></el-image>
</div>
</div>
</div>
</div>
</template>
</classHeader>
<ul v-if="currentCategory" v-infinite-scroll="load" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<span class="types__header-back" @click="back"><i class="iconfont icon-right"></i>{{ currentCategory.name }}</span>
<classHeader :is-back="true" @back="back">{{ currentCategory.name }}</classHeader>
<el-space fill wrap :fillRatio="30" direction="horizontal" class="list">
<div v-for="(item, i) in list" :key="i + 'i'" class="list__item" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<!-- <edit-model :isComp="true" @action="action($event, item, i)"> -->
<el-image class="list__img" :src="item.cover" fit="contain"> </el-image>
<el-image class="list__img" :src="item.cover" fit="contain" lazy loading="lazy" />
<!-- </edit-model> -->
</div>
</el-space>
@ -47,20 +42,22 @@
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, watch } from 'vue'
// import { ElDivider } from 'element-plus'
import api from '@/api'
import { mapActions } from 'vuex'
import { useStore } from 'vuex'
import getComponentsData from '@/common/methods/DesignFeatures/setComponents'
import DragHelper from '@/common/hooks/dragHelper'
import setImageData from '@/common/methods/DesignFeatures/setImage'
const dragHelper = new DragHelper()
let isDrag = false
let startPoint = { x: 99999, y: 99999 }
let tempDetail: any = null
import setItem2Data from '@/common/methods/DesignFeatures/setImage'
export default defineComponent({
// components: { ElDivider },
components: {},
setup(props) {
//
const dragHelper = new DragHelper()
let isDrag = false
let startPoint = { x: 99999, y: 99999 }
let tempDetail: any = null
//
const compsCache: any = {}
const state = reactive({
loading: false,
loadDone: false,
@ -70,12 +67,12 @@ export default defineComponent({
types: [],
showList: [],
})
const store = useStore()
const pageOptions = { type: 1, page: 0, pageSize: 20 }
onMounted(async () => {
if (state.types.length <= 0) {
const types = await api.material.getKinds({ type: 3 })
types.shift()
state.types = types
for (const iterator of types) {
const { list } = await api.home.getCompList({
@ -89,11 +86,11 @@ export default defineComponent({
})
const mouseup = (e: any) => {
e.preventDefault()
setTimeout(() => {
isDrag = false
tempDetail = null
startPoint = { x: 99999, y: 99999 }
}, 10)
// setTimeout(() => {
isDrag = false
tempDetail = null
startPoint = { x: 99999, y: 99999 }
// }, 10)
}
const mousemove = (e: any) => {
e.preventDefault()
@ -150,29 +147,31 @@ export default defineComponent({
state.currentCategory = null
}
return {
...toRefs(state),
load,
action,
back,
selectTypes,
mouseup,
mousemove,
const dragStart = async (e: any, { id, width, height, cover }: any) => {
startPoint = { x: e.x, y: e.y }
// tempDetail = await api.home.getTempDetail({ id, type: 1 })
// let finalWidth = tempDetail.width
//
const img = await setItem2Data({ width, height, url: cover })
dragHelper.start(e, img.canvasWidth)
tempDetail = await getCompDetail({ id, type: 1 })
if (Array.isArray(JSON.parse(tempDetail.data))) {
store.commit('selectItem', { data: JSON.parse(tempDetail.data), type: 'group' })
} else {
store.commit('selectItem', { data: JSON.parse(tempDetail.data), type: 'text' })
}
}
},
methods: {
...mapActions(['addGroup', 'addWidget']),
async selectItem(item: any) {
const selectItem = async (item: any) => {
if (isDrag) {
return
}
this.$store.commit('setShowMoveable', false) //
tempDetail = tempDetail || (await api.home.getTempDetail({ id: item.id, type: 1 }))
store.commit('setShowMoveable', false) //
tempDetail = tempDetail || (await getCompDetail({ id: item.id, type: 1 }))
// let group = JSON.parse(tempDetail.data)
const group: any = await getComponentsData(tempDetail.data)
let parent: any = { x: 0, y: 0 }
const { width: pW, height: pH } = this.$store.getters.dPage
console.log(group)
const { width: pW, height: pH } = store.getters.dPage
Array.isArray(group) &&
group.forEach((element: any) => {
@ -183,29 +182,38 @@ export default defineComponent({
element.left += (pW - parent.width) / 2
element.top += (pH - parent.height) / 2
})
this.addGroup(group)
store.dispatch('addGroup', group)
} else {
group.text && (group.text = decodeURIComponent(group.text))
group.left = pW / 2 - group.fontSize * (group.text.length / 2)
group.top = pH / 2 - group.fontSize / 2
this.addWidget(group)
store.dispatch('addWidget', group)
}
},
async dragStart(e: any, { id }: any) {
startPoint = { x: e.x, y: e.y }
tempDetail = await api.home.getTempDetail({ id, type: 1 })
let finalWidth = tempDetail.width
if (finalWidth) {
const img = await setImageData(tempDetail)
finalWidth = img.canvasWidth
}
dragHelper.start(e, finalWidth)
if (Array.isArray(JSON.parse(tempDetail.data))) {
this.$store.commit('selectItem', { data: JSON.parse(tempDetail.data), type: 'group' })
} else {
this.$store.commit('selectItem', { data: JSON.parse(tempDetail.data), type: 'text' })
}
},
}
function getCompDetail(params: any) {
//
return new Promise((resolve) => {
if (compsCache[params.id]) {
resolve(compsCache[params.id])
} else api.home.getTempDetail(params).then((res: any) => {
resolve(res)
compsCache[params.id] = res //
})
})
}
return {
...toRefs(state),
load,
action,
back,
selectTypes,
mouseup,
mousemove,
dragStart,
selectItem,
}
},
})
</script>
@ -259,76 +267,9 @@ export default defineComponent({
font-size: 14px;
color: #999;
}
.types {
display: flex;
flex-wrap: wrap;
padding: 10px 0 0 6px;
&__item {
position: relative;
width: 64px;
// height: 44px;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-weight: 600;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
margin: 8px 4px 0 4px;
background-size: cover;
background-repeat: no-repeat;
text-shadow: 0 1px 0 rgb(0 0 0 / 25%);
opacity: 0.5;
}
&--select {
opacity: 1;
}
&__header {
user-select: none;
cursor: pointer;
margin-bottom: 12px;
font-size: 13px;
color: #333333;
display: flex;
align-items: center;
&-more {
display: flex;
align-items: center;
color: #a0a0a0;
font-size: 13px;
}
&-back {
cursor: pointer;
padding: 0 0 0 0.6rem;
display: flex;
align-items: center;
color: #333;
font-size: 16px;
height: 2.9rem;
position: absolute;
z-index: 2;
background: #ffffff;
width: 320px;
.icon-right {
transform: rotate(180deg);
}
}
}
}
.list-wrap {
display: flex;
justify-content: space-between;
margin-bottom: 1.8rem;
}
.content {
&__wrap {
padding: 0.5rem 1rem;
height: 100%;
overflow: auto;
padding-bottom: 100px;
}
}
</style>

View File

@ -2,38 +2,34 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 15:16:07
* @Description: 素材列表
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:18:30
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-24 11:14:28
-->
<template>
<div class="wrap">
<search-header v-model="searchKeyword" type="none" @change="searchChange" />
<div style="height: 0.5rem" />
<!-- <div class="types">
<div v-for="(t, ti) in types" :key="ti + 't'" :style="{ backgroundColor: colors[ti] }" :class="['types__item', { 'types--select': currentType === t.id }]" @click="selectType(t)"></div>
</div> -->
<!-- <div class="tags">
<el-check-tag v-for="(t2, t2i) in sub" :key="t2i + 't2'" :checked="t2.id === currentCheck" class="tags__item" @click="tagsChange(t2.id)">{{ t2.name }}</el-check-tag>
</div> -->
<div v-show="!currentCategory" class="content__wrap">
<div v-for="(t, ti) in types" :key="ti + 't'">
<div v-if="showList[ti]?.length > 0" class="types__header" @click="selectTypes(t)">
<span style="flex: 1">{{ t.name }}</span>
<span class="types__header-more">全部<i class="iconfont icon-right"></i></span>
</div>
<div v-else class="loading"><i class="el-icon-loading" /> 拼命加载中</div>
<classHeader v-show="!currentCategory" :types="types" @select="selectTypes">
<template v-slot="{ index }">
<div class="list-wrap">
<div v-for="(item, i) in showList[ti]" :key="i + 'sl'" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img-thumb" :src="item.thumb" fit="contain"></el-image>
<div v-for="(item, i) in showList[index]" :key="i + 'sl'" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img-thumb" :src="item.thumb" fit="contain" lazy loading="lazy" />
</div>
</div>
</div>
</div>
</template>
</classHeader>
<ul v-if="currentCategory" v-infinite-scroll="load" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<span class="types__header-back" @click="back"><i class="iconfont icon-right"></i>{{ currentCategory.name }}</span>
<classHeader :is-back="true" @back="back">{{ currentCategory.name }}</classHeader>
<el-space fill wrap :fillRatio="30" direction="horizontal" class="list">
<div v-for="(item, i) in list" :key="i + 'i'" class="list__item" draggable="false" @mousedown="dragStart($event, item)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="selectItem(item)" @dragstart="dragStart($event, item)">
<el-image class="list__img" :src="item.thumb" fit="contain"></el-image>
<el-image class="list__img" :src="item.thumb" fit="contain" lazy loading="lazy" />
</div>
</el-space>
<div v-show="loading" class="loading"><i class="el-icon-loading" /> 拼命加载中</div>
@ -50,11 +46,13 @@ import wSvg from '../../widgets/wSvg/wSvg.vue'
import { mapActions, mapGetters } from 'vuex'
import setImageData from '@/common/methods/DesignFeatures/setImage'
import DragHelper from '@/common/hooks/dragHelper'
let isDrag = false
let startPoint = { x: 99999, y: 99999 }
const dragHelper = new DragHelper()
export default defineComponent({
components: {},
props: ['active'],
setup(props) {
const colors = ['#f8704b', '#5b89ff', '#2cc4cc', '#a8ba73', '#f8704b']
@ -76,7 +74,6 @@ export default defineComponent({
onMounted(async () => {
if (state.types.length <= 0) {
const types = await api.material.getKinds({ type: 2 })
types.shift()
state.types = types
for (const iterator of types) {
const { list } = await api.material.getList({
@ -187,7 +184,8 @@ export default defineComponent({
},
async dragStart(e: any, item: any) {
startPoint = { x: e.x, y: e.y }
const img = await setImageData(item)
const { width, height, thumb, url } = item
const img = await setImageData({ width, height, url: thumb || url })
dragHelper.start(e, img.canvasWidth)
this.$store.commit('selectItem', { data: { value: item }, type: item.type })
},
@ -200,65 +198,6 @@ export default defineComponent({
width: 100%;
height: 100%;
}
.types {
display: flex;
flex-wrap: wrap;
padding: 10px 0 0 6px;
&__item {
position: relative;
width: 64px;
// height: 44px;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-weight: 600;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
margin: 8px 4px 0 4px;
background-size: cover;
background-repeat: no-repeat;
text-shadow: 0 1px 0 rgb(0 0 0 / 25%);
opacity: 0.5;
}
&--select {
opacity: 1;
}
&__header {
user-select: none;
cursor: pointer;
margin-bottom: 12px;
font-size: 13px;
color: #333333;
display: flex;
align-items: center;
&-more {
display: flex;
align-items: center;
color: #a0a0a0;
font-size: 13px;
}
&-back {
cursor: pointer;
padding: 0 0 0 0.6rem;
display: flex;
align-items: center;
color: #333;
font-size: 16px;
height: 2.9rem;
position: absolute;
z-index: 2;
background: #ffffff;
width: 320px;
.icon-right {
transform: rotate(180deg);
}
}
}
}
.tags {
padding: 20px 0 0 10px;
&__item {
@ -309,13 +248,4 @@ export default defineComponent({
font-size: 14px;
color: #999;
}
.content {
&__wrap {
padding: 1rem;
height: 100%;
overflow: auto;
padding-bottom: 100px;
}
}
</style>

View File

@ -1,80 +1,122 @@
<!--
* @Author: ShawnPhang
* @Date: 2022-02-11 18:48:23
* @Description:
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:18:14
* @Description: 照片图库 Form:Unsplash无版权图片
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 19:04:39
-->
<template>
<div class="wrap">
<search-header type="none" @change="searchChange" />
<photo-list :isDone="loadDone" :listData="recommendImgList" @load="getDataList" @drag="dragStart" @select="selectImg" />
<div style="height: 0.5rem" />
<classHeader v-show="!currentCategory" :types="types" @select="selectTypes">
<template v-slot="{ index }">
<photo-list :isShort="true" :listData="showList[index]" @load="getDataList" @drag="dragStart($event, showList[index])" @select="selectImg($event, showList[index])" />
</template>
</classHeader>
<div v-if="currentCategory">
<classHeader :is-back="true" @back="back">{{ currentCategory.name }}</classHeader>
<br /><br /><br />
<div style="margin: 0 1rem; height: 100vh">
<photo-list :isDone="loadDone" :listData="recommendImgList" @load="getDataList" @drag="dragStart" @select="selectImg" />
</div>
</div>
</div>
</template>
<script>
//
const NAME = 'img-list-wrap'
import { toRefs, reactive, computed, onMounted } from 'vue'
import wImage from '../../widgets/wImage/wImage.vue'
import api from '@/api'
import { mapActions, mapGetters } from 'vuex'
import { useStore } from 'vuex'
import setImageData from '@/common/methods/DesignFeatures/setImage'
// import imgList from './components/imgList.vue'
export default {
name: NAME,
// components: { imgList },
components: {},
props: ['active'],
data() {
return {
setup() {
const store = useStore()
const state = reactive({
recommendImgList: [],
// loading: false,
loadDone: false,
page: 0,
}
},
computed: {
...mapGetters(['dPage']),
},
currentCategory: null,
types: [],
showList: [],
})
const dPage = computed(() => store.getters.dPage)
let loading = false
mounted() {
this.getDataList()
},
methods: {
...mapActions(['addWidget']),
async getDataList() {
if (this.loadDone || this.loading) {
return
onMounted(async () => {
if (state.types.length <= 0) {
const types = await api.material.getKinds({ type: 4 })
state.types = types
for (const iterator of types) {
const { list } = await api.material.getImagesList({ cate: iterator.id, pageSize: 2 })
state.showList.push(list)
}
}
this.loading = true
this.page += 1
let { list = [], total } = await api.material.getImagesList({ page: this.page, pageSize: 30 })
list.length <= 0 ? (this.loadDone = true) : (this.recommendImgList = this.recommendImgList.concat(list))
setTimeout(() => {
this.loading = false
}, 100)
},
async selectImg(index) {
const item = this.recommendImgList[index]
this.$store.commit('setShowMoveable', false) //
})
const selectImg = async (index, list) => {
const item = list ? list[index] : state.recommendImgList[index]
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wImage.setting))
const img = await setImageData(item) // await getImage(item.url)
setting.width = img.width
setting.height = img.height // parseInt(100 / item.value.ratio, 10)
setting.imgUrl = item.url
const { width: pW, height: pH } = this.dPage
const { width: pW, height: pH } = dPage
setting.left = pW / 2 - img.width / 2
setting.top = pH / 2 - img.height / 2
this.addWidget(setting)
},
dragStart(index) {
const item = this.recommendImgList[index]
this.$store.commit('selectItem', { data: { value: item }, type: 'image' })
},
searchChange(e) {
store.dispatch('addWidget', setting)
}
const getDataList = async () => {
if (state.loadDone || loading) {
return
}
loading = true
state.page += 1
let { list = [], total } = await api.material.getImagesList({ cate: state.currentCategory.id, page: state.page, pageSize: 30 })
list.length <= 0 ? (state.loadDone = true) : (state.recommendImgList = state.recommendImgList.concat(list))
setTimeout(() => {
loading = false
}, 100)
}
const dragStart = (index, list) => {
const item = list ? list[index] : state.recommendImgList[index]
store.commit('selectItem', { data: { value: item }, type: 'image' })
}
const searchChange = (e) => {
console.log(e)
},
}
const selectTypes = (item) => {
state.currentCategory = item
getDataList()
}
const back = () => {
state.currentCategory = null
state.page = 0
state.loadDone = false
state.recommendImgList = []
}
return {
...toRefs(state),
selectImg,
getDataList,
dragStart,
searchChange,
selectTypes,
back,
}
},
}
</script>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 15:16:07
* @Description: 模板列表
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-04 00:00:54
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-22 09:55:59
-->
<template>
<div class="wrap">
@ -22,7 +22,6 @@
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, ref } from 'vue'
import { ElDivider } from 'element-plus'
import api from '@/api'
import { mapActions, mapGetters, useStore } from 'vuex'
import { useRoute } from 'vue-router'
@ -32,7 +31,7 @@ import searchHeader from './components/searchHeader.vue'
import useConfirm from '@/common/methods/confirm'
export default defineComponent({
components: { ElDivider, searchHeader },
components: { searchHeader },
setup() {
const listRef = ref(null)
const route = useRoute()

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-02-11 18:48:23
* @Description: 组件列表
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:17:46
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-13 18:48:25
-->
<template>
<div class="wrap">
@ -12,9 +12,10 @@
<i class="icon sd-w-qrcode" />
<div class="text"><span>二维码</span><span class="desc">在设计中使用风格化二维码</span></div>
</div>
<div class="header">其它</div>
<div class="item" @click="openImageCutout">
<i class="icon sd-w-table" />
<div class="text"><span>AI抠图</span> <span class="desc">上传图像一键去除背景</span></div>
<i class="icon sd-AI_zhineng" />
<div class="text"><span>智能抠图</span> <span class="desc">上传图像一键去除背景</span></div>
</div>
<imageCutout ref="imageCutout" />
</div>
@ -55,11 +56,6 @@ export default {
}
this.loading = true
this.page += 1
// let { list = [], total } = await api.material.getImagesList({ page: this.page, pageSize: 30 })
// list.length <= 0 ? (this.loadDone = true) : (this.recommendImgList = this.recommendImgList.concat(list))
// setTimeout(() => {
// this.loading = false
// }, 100)
},
addQrcode() {
this.$store.commit('setShowMoveable', false) //

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-02-13 22:18:35
* @Description: 我的
* @LastEditors: rayadaschn 115447518+rayadaschn@users.noreply.github.com
* @LastEditTime: 2023-09-01 14:17:23
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 11:50:34
-->
<template>
<div class="wrap">
@ -11,16 +11,18 @@
<el-tab-pane label="资源管理" name="pics"> </el-tab-pane>
<el-tab-pane label="我的作品" name="design"> </el-tab-pane>
</el-tabs>
<div v-show="tabActiveName === 'pics'" class="wrap">
<div v-show="tabActiveName === 'pics'">
<uploader v-model="percent" class="upload" @done="uploadDone">
<el-button class="upload-btn" plain>上传图片 <i class="iconfont icon-upload" /></el-button>
</uploader>
<el-button class="upload-btn upload-psd" plain type="primary" @click="openPSD">上传 PSD 模板</el-button>
<photo-list ref="imgListRef" :edit="editOptions" :isDone="isDone" :listData="imgList" @load="load" @drag="dragStart" @select="selectImg" />
<div style="margin: 1rem; height: 100vh">
<photo-list ref="imgListRef" :edit="editOptions.photo" :isDone="isDone" :listData="imgList" @load="load" @drag="dragStart" @select="selectImg" />
</div>
</div>
<div v-show="tabActiveName === 'design'" class="wrap">
<ul ref="listRef" v-infinite-scroll="loadDesign" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<img-water-fall :listData="designList" @select="selectDesign" />
<img-water-fall :edit="editOptions.works" :listData="designList" @select="selectDesign" />
<!-- <div v-show="loading" class="loading"><i class="el-icon-loading"></i>拼命加载中..</div> -->
<div v-show="isDone" class="loading">全部加载完毕</div>
</ul>
@ -143,12 +145,29 @@ export default defineComponent({
api.material.deleteMyPhoto({ id: item.id, key })
state.imgListRef.delItem(i) //
}
state.editOptions = [
const deleteWorks = async ({ i, item }: any) => {
const isPass = await useConfirm('警告', '删除后不可找回,请确认操作', 'warning')
if (isPass) {
await api.material.deleteMyWorks({ id: item.id })
setTimeout(() => {
router.push({ path: '/home', query: { }, replace: true })
loadDesign(true)
}, 300);
}
}
state.editOptions = {
photo: [
{
name: '删除',
fn: deleteImg,
},
],works: [
{
name: '删除',
fn: deleteWorks,
},
]
}
const dragStart = (index: number) => {
const item = state.imgList[index]
store.commit('selectItem', { data: { value: item }, type: 'image' })

View File

@ -0,0 +1,109 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-10-04 02:04:04
* @Description: 列表分类头部
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 02:30:59
-->
<template>
<div v-if="!isBack" class="content__wrap">
<div v-for="(t, ti) in types" :key="ti + 't'">
<div class="types__header" @click="select(t)">
<span style="flex: 1">{{ t.name }}</span>
<span class="types__header-more">全部<i class="iconfont icon-right"></i></span>
</div>
<slot :index="ti" />
</div>
</div>
<span v-else class="types__header-back" @click="back">
<i class="iconfont icon-right"></i>
<slot />
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: ['types', 'isBack'],
emits: ['select', 'back'],
setup(props, { emit }) {
const select = (item: any) => {
emit('select', item)
}
const back = () => {
emit('back')
}
return { select, back }
},
})
</script>
<style lang="less" scoped>
.content {
&__wrap {
padding: 0.5rem 1rem;
height: 100%;
overflow: auto;
padding-bottom: 100px;
}
}
.types {
display: flex;
flex-wrap: wrap;
padding: 10px 0 0 6px;
&__item {
position: relative;
width: 64px;
// height: 44px;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-weight: 600;
font-size: 13px;
border-radius: 4px;
cursor: pointer;
margin: 8px 4px 0 4px;
background-size: cover;
background-repeat: no-repeat;
text-shadow: 0 1px 0 rgb(0 0 0 / 25%);
opacity: 0.5;
}
&--select {
opacity: 1;
}
&__header {
user-select: none;
cursor: pointer;
margin-bottom: 12px;
font-size: 13px;
color: #333333;
display: flex;
align-items: center;
&-more {
display: flex;
align-items: center;
color: #a0a0a0;
font-size: 13px;
}
&-back {
cursor: pointer;
padding: 0 0 0 0.6rem;
display: flex;
align-items: center;
color: #333;
font-size: 16px;
height: 2.9rem;
position: absolute;
z-index: 2;
background: #ffffff;
width: 320px;
.icon-right {
transform: rotate(180deg);
}
}
}
}
</style>

View File

@ -2,19 +2,17 @@
* @Author: ShawnPhang
* @Date: 2022-01-11 17:54:14
* @Description: 模板编辑组件
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-24 15:00:36
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-02-11 00:07:36
-->
<template>
<div class="wrap">
<slot />
<div class="showMask" @click.stop="">
<el-dropdown placement="bottom-end">
<el-dropdown placement="bottom-end" :show-arrow="false">
<i class="iconfont icon-more"></i>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item @click="action('edit')">编辑</el-dropdown-item>
<el-dropdown-item @click="action('del')">删除</el-dropdown-item> -->
<el-dropdown-item v-for="(op, oi) in options" :key="oi + 'o'" @click="op.fn(data)">{{ op.name }}</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -56,6 +54,8 @@ export default defineComponent({
<style lang="less" scoped>
.wrap {
width: 100%;
height: 100%;
position: relative;
}
.wrap:hover > .showMask {

View File

@ -0,0 +1,31 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-10-04 19:12:40
* @Description: 图片描述ToolTip
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 22:51:06
-->
<template>
<el-tooltip :disabled="!detail.author" :offset="1" effect="light" placement="bottom-start" :hide-after="0" :enterable="false" raw-content>
<template #content>
<p style="max-width: 140px">
<b>{{ detail.description }}</b>
</p>
<p>@{{ detail.author }}</p>
</template>
<slot />
</el-tooltip>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
detail: {},
},
setup() {
return {}
},
})
</script>

View File

@ -2,92 +2,95 @@
* @Author: ShawnPhang
* @Date: 2021-12-16 16:20:16
* @Description: 瀑布流组件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-06 17:58:49
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 11:45:24
-->
<template>
<div ref="imgWaterFall" :style="{ height: countHeight + 'px' }" class="img-water-fall">
<!-- backgroundImage: `url(${item.cover})` -->
<div v-for="(item, index) in list" :key="index + 'iwf'" :style="{ top: item.top + 'px', left: item.left + 'px', width: width + 'px', height: item.height + 'px' }" class="img-box" @click.stop="selectItem(item, index)">
<v-lazy-image v-if="!item.fail" class="img" @error="loadError(item)" :src="item.cover" />
<div class="fail_img" v-else>{{ item.title }}</div>
<div v-for="(item, i) in list" :key="i + 'iwf'" :style="{ top: item.top + 'px', left: item.left + 'px', width: width + 'px', height: item.height + 'px' }" class="img-box" @click.stop="selectItem(item, i)">
<edit-model v-if="edit" :options="edit" :data="{ item, i }">
{{ item.isDelect }}
<div v-if="item.isDelect" class="list__mask">已删除</div>
<el-image v-if="!item.fail" class="img" :src="item.cover" lazy loading="lazy" @error="loadError(item)" />
<div v-else class="fail_img">{{ item.title }}</div>
</edit-model>
<el-image v-else class="img" :src="item.cover" lazy loading="lazy" @error="loadError(item)" />
</div>
</div>
</template>
<script>
import vLazyImage from 'v-lazy-image'
const NAME = 'img-water-fall'
// import api from '@/api/album'
const columnHeights = [] //
const columnNums = 2 //
const gap = 7 //
import { defineComponent, toRefs, reactive, watch } from 'vue'
export default {
export default defineComponent({
name: NAME,
components: { vLazyImage },
props: {
listData: {
type: Array,
required: true,
},
// scroll: {
// default: true,
// },
edit: {}
},
emits: ['select', 'load'],
data() {
return {
setup(props, { emit }) {
const state = reactive({
width: 146, //
list: [],
countHeight: 0,
})
const columnHeights = [] //
const columnNums = 2 //
const gap = 7 //
watch(
() => props.listData,
() => {
columnHeights.length = 0
const widthLimit = state.width * columnNums // + gap * (columnNums - 1) // 每行宽度
const cloneList = JSON.parse(JSON.stringify(props.listData))
for (let i = 0; i < cloneList.length; i++) {
let index = i % columnNums
const item = cloneList[i]
item.height = (item.height / item.width) * state.width //
item.left = index * (widthLimit / columnNums + gap) //
item.top = columnHeights[index] + gap || 0 //
// columnHeights[index] = isNaN(columnHeights[index]) ? item.height : item.height + columnHeights[index] + gap //
//
if (isNaN(columnHeights[index])) {
columnHeights[index] = item.height
} else {
index = columnHeights.indexOf(Math.min(...columnHeights))
item.left = index * (widthLimit / columnNums + gap)
item.top = columnHeights[index] + gap || 0
columnHeights[index] = item.height + columnHeights[index] + gap
}
}
state.countHeight = Math.max(...columnHeights)
state.list = cloneList
},
)
const load = () => {
emit('load')
}
const selectItem = (value, index) => {
emit('select', value)
}
const loadError = (item) => {
item.fail = true
}
return {
...toRefs(state),
load,
selectItem,
loadError,
}
},
// async mounted() {
// },
watch: {
listData() {
columnHeights.length = 0
const widthLimit = this.width * columnNums // + gap * (columnNums - 1) // 每行宽度
const cloneList = JSON.parse(JSON.stringify(this.listData))
let lineWidth = 0
let index = 0
for (let i = 0; i < cloneList.length; i++) {
const item = cloneList[i]
const r = item.width / this.width
item.height = item.height / r
lineWidth += this.width
if (lineWidth >= widthLimit) {
index++
item.left = this.width + gap
item.top = columnHeights[index] + gap || 0
columnHeights[index] = typeof columnHeights[index] === 'number' ? item.height + columnHeights[index] + gap : item.height
// init
lineWidth = 0
index = 0
} else {
item.top = columnHeights[index] + gap || 0
item.left = 0
columnHeights[index] = typeof columnHeights[index] === 'number' ? item.height + columnHeights[index] + gap : item.height
}
}
this.countHeight = Math.max(...columnHeights)
this.list = cloneList
},
},
methods: {
load() {
this.$emit('load')
},
selectItem(value, index) {
this.$emit('select', value)
},
loadError(item) {
item.fail = true
},
},
}
})
</script>
<style lang="less" scoped>
@ -128,4 +131,17 @@ export default {
pointer-events: none;
}
}
.list {
&__mask {
position: absolute;
z-index: 2;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@ -2,21 +2,26 @@
* @Author: ShawnPhang
* @Date: 2022-02-23 15:48:52
* @Description: 图片列表组件 Bookshelf Layout
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-17 15:20:25
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 11:12:04
-->
<template>
<ul ref="listRef" class="img-list-wrap" style="overflow: auto" @scroll="scrollEvent($event)">
<ul ref="listRef" class="img-list-wrap" :style="{ paddingBottom: isShort ? '15px' : '200px' }" @scroll="scrollEvent($event)">
<div class="list">
<div v-for="(item, i) in list" :key="i + 'i'" :style="{ width: item.listWidth + 'px' }" class="list__img" draggable="false" @mousedown="dragStart($event, i)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="select(i)" @dragstart="dragStart($event, i)">
<div v-for="(item, i) in list" :key="i + 'i'" :style="{ width: item.listWidth + 'px', marginRight: item.gap + 'px' }" class="list__img" draggable="false" @mousedown="dragStart($event, i)" @mousemove="mousemove" @mouseup="mouseup" @click.stop="select(i)" @dragstart="dragStart($event, i)">
<edit-model v-if="edit" :options="edit" :data="{ item, i }">
<div v-if="item.isDelect" class="list__mask">已删除</div>
<img class="img" :src="item.thumb || item.cover || item.url" />
<el-image class="img transparent-bg" :src="item.thumb || item.url" :style="{ height: getInnerHeight(item) + 'px' }" lazy loading="lazy" />
</edit-model>
<template v-else>
<img class="img" :src="item.thumb || item.cover || item.url" />
<imageTip :detail="item">
<el-image class="img" :src="item.thumb || item.url" :style="{ height: getInnerHeight(item) + 'px' }" lazy loading="lazy">
<template #placeholder>
<div :style="{ backgroundColor: item.color }" class="image-color" />
</template>
</el-image>
</imageTip>
</template>
<!-- <el-image :src="item.thumb || item.url" fit="cover"></el-image> -->
</div>
</div>
<div v-if="!isDone" v-show="loading" class="loading"><i class="el-icon-loading" /> 拼命加载中</div>
@ -34,6 +39,9 @@ export default defineComponent({
listData: {},
edit: {},
isDone: {},
isShort: {
default: false,
},
},
emits: ['load', 'drag', 'select'],
setup(props, context) {
@ -63,14 +71,15 @@ export default defineComponent({
watch(
() => props.listData,
async (newList: any, oldList: any) => {
!oldList && (oldList = [])
if (newList.length <= 0) {
state.list.length = 0
return
}
let list = newList.filter((v: any) => !newList.includes(v) || !oldList.includes(v)) // difference
list = JSON.parse(JSON.stringify(list))
const limitWidth = (await getFatherWidth()) - 32 // 296 // 256
const marginRight = 6 //
const limitWidth = (await getFatherWidth()) - marginRight
const standardHeight = 280 //
const neatArr: any = [] //
function factory(cutArr: any) {
@ -103,8 +112,9 @@ export default defineComponent({
}
const { list: newList, height }: any = await factory([list.shift()])
neatArr.push(
newList.map((x: any) => {
newList.map((x: any, index) => {
x.listWidth = (x.width / x.height) * height
x.gap = index !== newList.length - 1 ? marginRight : 0
return x
}),
)
@ -126,6 +136,7 @@ export default defineComponent({
}
function getRef() {
// ref
return state.listRef
}
@ -154,6 +165,9 @@ export default defineComponent({
load()
}
}
const getInnerHeight = ({ height, listWidth, width }: any) => (height * listWidth) / width
return {
load,
dragStart,
@ -164,6 +178,7 @@ export default defineComponent({
getRef,
mouseup,
mousemove,
getInnerHeight,
}
},
})
@ -172,23 +187,29 @@ export default defineComponent({
<style lang="less" scoped>
.img-list-wrap {
height: 100%;
margin-top: 14px;
// overflow-y: scroll;
padding-bottom: 200px;
overflow: auto;
}
.img {
transform-origin: center;
display: block;
width: 100%;
height: 100%;
}
.image-color {
width: 100%;
height: 100%;
animation: breathe 600ms ease-out infinite alternate;
}
.list {
position: relative;
padding: 4px 0 0 14px;
// padding: 4px 0 0 14px;
padding: 4px 0 0 0;
&__img {
background: #f1f2f4;
// background: #f1f2f4;
display: inline-block;
cursor: pointer;
margin: 0 6px 2px 0;
// margin: 0 6px 2px 0;
margin-bottom: 3px;
border-radius: 2px;
overflow: hidden;
position: relative;
@ -201,9 +222,10 @@ export default defineComponent({
}
&__mask {
position: absolute;
z-index: 2;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.5);
color: #ffffff;
display: flex;
align-items: center;
@ -216,4 +238,13 @@ export default defineComponent({
font-size: 14px;
color: #999;
}
/* 呼吸效果 */
@keyframes breathe {
0% {
opacity: 0.8;
}
100% {
opacity: 1;
}
}
</style>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-01-27 11:05:48
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-29 16:50:02
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 01:53:10
-->
<template>
<div class="search__wrap">
@ -19,7 +19,7 @@
</el-dropdown>
<span v-else style="width: 1rem"></span>
<el-input size="large" v-model="searchValue" placeholder="输入关键词搜索" class="input-with-select">
<el-input v-model="searchValue" size="large" placeholder="输入关键词搜索" class="input-with-select">
<template #append>
<el-button><i class="iconfont icon-search"></i></el-button>
</template>
@ -46,6 +46,7 @@ export default defineComponent({
if (props.type != 'none') {
api.home.getCategories({ type: 1 }).then((list: any) => {
list.unshift({ id: 0, name: '全部' })
state.materialCates = list
const { cate } = route.query
cate && (state.currentIndex = cate)

View File

@ -2,125 +2,253 @@
* @Author: ShawnPhang
* @Date: 2021-08-09 14:00:23
* @Description: 文字特效选择框组件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-29 17:52:31
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-30 10:14:30
-->
<template>
<el-card class="box-card" shadow="hover" :body-style="{ padding: effectSelect ? '20px' : 0 }">
<el-card class="box-card" shadow="hover" :body-style="{ padding: '20px' }">
<template #header>
<div class="card-header">
<!-- <template v-if="effectSelect">
<component :is="effectSelect" class="demo" />
</template> -->
<div v-show="effectSelect" style="position: relative; margin-right: 24px">
<div
:style="{
position: 'relative',
width: '27px',
fontSize: '27px',
color: data.color,
fontWeight: data.fontWeight,
fontStyle: data.fontStyle,
textDecoration: data.textDecoration,
opacity: data.opacity,
backgroundColor: data.backgroundColor,
}"
>
<div
v-for="(ef, efi) in data"
v-for="(ef, efi) in modelValue"
:key="efi + 'effect'"
:style="{
color: ef.filling && ef.filling.type === 2 ? 'transparent' : ef.filling ? ef.filling.color : undefined,
webkitTextStroke: ef.stroke ? `${ef.stroke.width}px ${ef.stroke.color}` : undefined,
textShadow: ef.shadow ? `${ef.shadow.offsetX}px ${ef.shadow.offsetY}px ${ef.shadow.blur}px ${ef.shadow.color}` : undefined,
backgroundImage: ef.filling && ef.filling.type === 2 ? `linear-gradient(${ef.filling.gradient.angle}deg, ${ef.filling.gradient.stops[0].color} ${Number(ef.filling.gradient.stops[0].offset) * 100}%, ${ef.filling.gradient.stops[1].color} ${Number(ef.filling.gradient.stops[1].offset) * 100}%)` : undefined,
webkitBackgroundClip: ef.filling && ef.filling.type === 2 ? 'text' : undefined,
color: ef.filling && ef.filling.enable && ef.filling.type === 0 ? ef.filling.color : 'transparent',
webkitTextStroke: ef.stroke && ef.stroke.enable ? `${ef.stroke.width / coefficient}px ${ef.stroke.color}` : undefined,
textShadow: ef.shadow && ef.shadow.enable ? `${ef.shadow.offsetX / coefficient}px ${ef.shadow.offsetY / coefficient}px ${ef.shadow.blur / coefficient}px ${ef.shadow.color}` : undefined,
backgroundImage: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : getGradientOrImg(ef)) : undefined,
webkitBackgroundClip: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : 'text') : undefined,
}"
class="demo"
>
A
</div>
A
</div>
<div v-show="!effectSelect">A</div>
<span class="title">文字特效</span>
<el-popover :visible="visiable" placement="left" :width="220" trigger="click">
<div class="select__box">
<div class="select__box__select-item" @click="select(null, { color: '' })"></div>
<div class="select__box__select-item">
<div class="e-box" @click="select('effect', { color: '#ffffff' })" />
<div class="select__box__select-item" @click="selectEffect(null)"></div>
<div v-for="(l, li) in list" :key="'list' + li" class="select__box__select-item" @click="selectEffect(l.id)">
<img :src="l.cover" />
</div>
</div>
<template #reference>
<el-button class="button" link @click="visiable = !visiable">{{ visiable ? '取消' : '选择' }}</el-button>
<el-button class="button" link @click="openSet">{{ visiable ? '取消' : '选择' }}</el-button>
</template>
</el-popover>
</div>
</template>
<!-- filling 描边 stroke 阴影 shadow -->
<div v-for="(f, fi) in data" v-show="effectSelect" :key="'f' + fi" class="feature__wrap">
<template v-if="f.filling && f.filling.type == 0">
<div class="feature__header">填充</div>
<color-select v-model="f.filling.color" class="feature__color" label="" @finish="(value) => finish('color', value)" />
<div v-show="layers && layers.length > 0" class="text item"><span style="width: 65px">强度</span> <el-slider v-model="strength" show-input :maxValue="100" input-size="small" :show-input-controls="false" @input="strengthChange"> </el-slider></div>
<el-collapse-item>
<template #title>
<b>高级编辑</b>
</template>
<template v-if="f.stroke">
<div class="feature__header">描边</div>
<color-select v-model="f.stroke.color" class="feature__color" label="" @finish="(value) => finish('color', value)" />
<div class="text item"><span class="strength">强度</span> <el-slider v-model="f.stroke.width" show-input input-size="small" :show-input-controls="false"> </el-slider></div>
</template>
<template v-if="f.shadow">
<div class="feature__header">阴影</div>
<color-select v-model="f.shadow.color" class="feature__color" label="" @finish="(value) => finish('color', value)" />
<div class="text item"><span class="strength">模糊</span> <el-slider v-model="f.shadow.blur" show-input input-size="small" :show-input-controls="false"> </el-slider></div>
</template>
</div>
<div class="line"></div>
<div style="display: flex; justify-content: space-between">
<el-button class="add-layer" size="small" type="primary" link @click="addLayer"> + 新建特效层</el-button> <el-button v-show="layers && layers.length > 0" class="add-layer" size="small" type="primary" link @click="unfold = !unfold">{{ unfold ? '收起' : '展开' }}全部</el-button>
</div>
<div class="line"></div>
<draggable v-model="layers" handle=".sd-yidong" item-key="uuid" v-bind="dragOptions">
<template #item="{ element, index }">
<div class="feature__grab-wrap">
<div class="layer__title">
<i class="icon sd-yidong" /><span style="font-size: 12px"><b>特效层</b> {{ index + 1 }}</span>
<i class="icon sd-delete" @click="removeLayer(index)" />
</div>
<div v-if="element.filling && [0, 2, '0', '2'].includes(element.filling.type)" v-show="unfold" class="feature__item">
<el-checkbox v-model="element.filling.enable" label="填充" class="feature__header" />
<color-select v-model="element.filling.color" width="28px" :modes="['纯色', '渐变']" label="" @change="colorChange($event, element.filling)" />
</div>
<div v-if="element.stroke" v-show="unfold" class="feature__item">
<el-checkbox v-model="element.stroke.enable" label="描边" class="feature__header" />
<el-input-number v-model="element.stroke.width" style="width: 65px; margin-right: 0.5rem" :min="0" size="small" controls-position="right" />
<color-select v-model="element.stroke.color" width="28px" label="" @finish="(value) => finish('color', value)" />
</div>
<div v-if="element.offset" v-show="unfold" class="feature__item">
<el-checkbox v-model="element.offset.enable" label="偏移" class="feature__header" />
<numberInput v-model="element.offset.x" style="width: 49.5px; margin-right: 2px" prepend="x" type="simple" />
<numberInput v-model="element.offset.y" style="width: 49.5px" prepend="y" type="simple" />
</div>
<div v-if="element.shadow" v-show="unfold" class="feature__item">
<el-checkbox v-model="element.shadow.enable" label="阴影" class="feature__header" />
<numberInput v-model="element.shadow.blur" prepend="blur" :minValue="0" style="width: 30px; margin-right: 2px" type="simple" />
<numberInput v-model="element.shadow.offsetX" prepend="x" style="width: 30px; margin-right: 2px" type="simple" />
<numberInput v-model="element.shadow.offsetY" prepend="y" style="width: 30px; margin-right: 0.5rem" type="simple" />
<color-select v-model="element.shadow.color" width="28px" label="" @finish="(value) => finish('color', value)" />
</div>
</div>
</template>
</draggable>
</el-collapse-item>
</el-card>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, watch, onMounted, nextTick } from 'vue'
import { defineComponent, toRefs, reactive, watch, onMounted, nextTick, computed } from 'vue'
import colorSelect from '../colorSelect.vue'
import { ElInputNumber, ElCheckbox } from 'element-plus'
import numberInput from '../numberInput.vue'
import draggable from 'vuedraggable'
import api from '@/api'
import getGradientOrImg from '../../widgets/wText/getGradientOrImg'
let froze_font_effect_list: any = []
export default defineComponent({
components: { colorSelect },
components: { colorSelect, ElInputNumber, numberInput, ElCheckbox, draggable },
props: ['modelValue', 'degree', 'data'],
emits: ['select'],
setup(props, context) {
emits: ['update:modelValue'],
setup(props, { emit }) {
const state = reactive({
strength: 20, //
effectSelect: '', //
strength: 50, //
visiable: false, //
list: [],
layers: [],
draging: false,
unfold: true,
})
const select = (value: string = '', style: any) => {
state.effectSelect = value
state.visiable = false
context.emit('select', { key: 'isEffect', value, style })
context.emit('select', { key: 'degree', value: 20 })
const dragOptions = {
animation: 300,
ghostClass: 'ghost',
chosenClass: 'choose',
}
const coefficient = computed(() => Math.round(160 / 27))
let rawData: any = [] //
onMounted(async () => {
await nextTick()
// state.effectSelect = props?.modelValue || ''
if (props.data && props.data.length > 0) {
state.effectSelect = 'true'
// console.log(props.data)
if (!props.data.textEffects) {
return
}
// state.strength = props?.degree || state.strength
const clone = JSON.parse(JSON.stringify(props.data.textEffects)) || []
state.layers = clone
.map((x: any) => {
x.uuid = String(Math.random())
return x
})
.reverse()
rawData = JSON.parse(JSON.stringify(state.layers))
})
// watch(
// () => props.degree,
// (value) => {
// state.strength = props?.degree || state.strength
// },
// )
watch(
() => props.modelValue,
(value) => {
state.effectSelect = value || ''
},
)
// watch(
// () => state.strength,
// (strength) => {
// context.emit('select', { key: 'degree', value: strength }) //
// },
// )
watch(
() => props.data,
() => {
state.effectSelect = props.data ? 'true' : ''
() => state.layers,
(v) => {
const newEffect = v.map((x) => {
delete x.uuid
return x
})
emit('update:modelValue', newEffect.reverse())
},
{ deep: true },
)
//
const selectEffect = async (id) => {
state.visiable = false
if (id) {
const { data } = await api.home.getTempDetail({ id, type: 1 })
state.layers = JSON.parse(data)
.textEffects.map((x) => {
x.uuid = String(Math.random())
return x
})
.reverse()
} else state.layers = []
}
//
const removeLayer = (i: number) => {
state.layers.splice(i, 1)
rawData = JSON.parse(JSON.stringify(state.layers))
}
//
const addLayer = () => {
const filling = { enable: false, type: 0, color: '#000000ff' }
const stroke = { enable: false, width: 0, color: '#000000ff', type: 'outer' }
const offset = { enable: false, x: 0, y: 0 }
const shadow = { enable: false, color: '#000000ff', offsetX: 0, offsetY: 0, blur: 0, opacity: 0 }
state.layers.unshift({ filling, stroke, shadow, offset, uuid: String(Math.random()) })
rawData = JSON.parse(JSON.stringify(state.layers))
}
const finish = () => {}
const colorChange = (e: any, item: any) => {
const modeStr: any = {
渐变: 2,
纯色: 0,
}
item.gradient = {
angle: e.angle,
stops: e.stops,
}
setTimeout(() => {
item.type = modeStr[e.mode] || 0
}, 100)
}
// const onMove = ({ relatedContext, draggedContext }: any) => {
// const relatedElement = relatedContext.element
// const draggedElement = draggedContext.element
// return (!relatedElement || relatedElement.parent == -1) && draggedElement.parent == -1
// }
const onDone = () => {
state.draging = false
}
const strengthChange = (x: any) => {
const effectScale = 1 + (x - 50) / 50
state.layers.forEach((item: any, index) => {
if (item.stroke) {
item.stroke.width = rawData[index].stroke.width * effectScale
}
if (item.shadow) {
item.shadow.blur = rawData[index].shadow.blur * effectScale
}
})
}
//
const openSet = async () => {
state.visiable = !state.visiable
if (froze_font_effect_list.length <= 0) {
const { list } = await api.home.getCompList({
cate: 12,
type: 1,
pageSize: 30,
})
state.list = list
froze_font_effect_list = list
} else state.list = froze_font_effect_list
}
return {
...toRefs(state),
select,
selectEffect,
finish,
coefficient,
removeLayer,
addLayer,
dragOptions,
onDone,
strengthChange,
openSet,
colorChange,
getGradientOrImg,
}
},
methods: {},
@ -128,19 +256,58 @@ export default defineComponent({
</script>
<style lang="less" scoped>
:deep(.el-input-group__prepend) {
padding: 0 8px;
}
:deep(.el-input-number__decrease) {
width: 18px;
}
:deep(.el-input-number__increase) {
width: 18px;
}
:deep(.el-input-number.is-controls-right .el-input__wrapper) {
padding-right: 32px;
}
:deep(.el-input-group__prepend) {
background: #ffffff;
}
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
color: #333333;
}
:deep(.el-collapse-item__header) {
border-bottom: none;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
.feature {
&__item {
display: flex;
align-items: center;
margin-top: 6px;
}
&__header {
font-size: 14px;
flex: 1;
padding: 12px 0 2px 0;
font-weight: 600;
color: #333333;
}
&__header:first-of-type {
padding: 0 0 2px 0;
}
&__grab-wrap {
position: relative;
padding: 10px 0;
}
&__grab-wrap:first-of-type {
padding-top: 0;
}
&__grab-wrap:last-of-type {
padding-bottom: 0;
}
&__wrap {
position: relative;
padding-top: 20px;
padding-top: 32px;
}
&__wrap::after {
position: absolute;
@ -148,7 +315,7 @@ export default defineComponent({
height: 1px;
width: 100%;
background: #e9e9e9;
top: 2px;
top: 16px;
}
&__wrap:first-of-type {
padding: 0;
@ -156,9 +323,6 @@ export default defineComponent({
&__wrap:first-of-type::after {
height: 0;
}
&__color {
margin: 8px 0 0 0;
}
}
.card-header {
@ -182,12 +346,11 @@ export default defineComponent({
}
.demo {
margin: 0 0 0 0.15rem;
font-size: 27px;
color: #ffffff;
outline: none;
position: absolute;
top: -15px;
top: 0;
left: 0;
width: 100%;
height: 100%;
@ -197,9 +360,6 @@ export default defineComponent({
font-weight: 600;
color: #33383e;
}
.strength {
width: 80px;
}
.select__box {
display: flex;
flex-wrap: wrap;
@ -208,17 +368,69 @@ export default defineComponent({
cursor: pointer;
position: relative;
height: 40px;
width: 25%;
width: 33%;
align-items: center;
justify-content: center;
display: flex;
.e-box {
font-size: 21px;
color: #ffffff;
}
}
&__select-item:hover {
background: rgba(0, 0, 0, 0.07);
}
}
.layer {
&__title {
display: flex;
align-items: center;
span {
flex: 1;
}
.sd-yidong {
cursor: grab !important;
margin-right: 6px;
}
.icon {
// display: none;
cursor: pointer;
}
.icon:hover {
transform: scale(1.1);
color: @active-text-color;
}
}
// &__title:hover > .icon {
// display: block;
// }
}
.add-layer {
// margin-top: 12px;
margin-bottom: 10px;
// width: 100%;
}
// 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;
}
.line {
margin-top: 8px;
height: 18px;
border-top: 1px solid #e9e9e9;
}
</style>

View File

@ -1,270 +0,0 @@
<template>
<div
:id="params.uuid"
ref="widget"
class="w-svg"
:style="{
position,
left: params.left - parent.left + 'px',
top: params.top - parent.top + 'px',
width: params.width + 'px',
height: params.height + 'px',
opacity: params.opacity,
}"
>
<!-- <div v-for="(img, i) in params.imgs" v-show="cropEdit" :key="i" :ref="params.uuid + '_ebox_' + i" :style="editBoxs[i]" class="svg__edit__wrap">
<img :img-key="i" class="edit__model" :src="img" />
</div> -->
<div v-show="cropEdit" :ref="params.uuid + '_ebox'" :style="editBoxStyle" class="svg__edit__wrap">
<img class="edit__model" :src="params.imgUrl" />
</div>
</div>
</template>
<script>
// svg
const NAME = 'w-svg'
import { mapGetters, mapActions } from 'vuex'
export default {
name: NAME,
setting: {
name: '图片容器',
type: NAME,
uuid: -1,
width: 300,
height: 300,
left: 0,
top: 0,
zoom: 1.5,
transform: '',
radius: 0,
opacity: 1,
parent: '-1',
svgUrl: '',
setting: [],
record: {
width: 0,
height: 0,
minWidth: 10,
minHeight: 10,
dir: 'all',
},
},
props: ['params', 'parent'],
data() {
return {
position: 'absolute', // 'absolute'relative
editBoxStyle: {
transformOrigin: 'center',
},
editBoxs: {},
editingKey: '',
cropWidgetXY: {}, //
}
},
computed: {
...mapGetters(['dActiveElement', 'dZoom', 'dMouseXY']),
tZoom() {
return this.params.zoom
},
cropEdit() {
return this.params.cropEdit
},
imgChange() {
return this.params.imgUrl
},
},
watch: {
async tZoom() {
await this.$nextTick()
this.updateRecord()
},
imgChange() {
// TODO
this.svgImg.attr({
'xlink:href': this.params.imgUrl,
})
},
cropEdit(val) {
// TODO
if (val) {
document.getElementById(this.params.uuid).addEventListener('mousedown', this.touchstart, false)
} else {
document.getElementById(this.params.uuid).removeEventListener('mousedown', this.touchstart, false)
}
},
},
updated() {
this.updateRecord()
},
async mounted() {
await this.$nextTick()
await this.loadSvg()
this.updateRecord()
// document.getElementById(this.params.uuid).addEventListener('mousedown', this.touchstart, false)
document.addEventListener('mouseup', this.touchend, false)
this.params.transform && (this.$refs.widget.style.transform = this.params.transform)
this.params.rotate && (this.$refs.widget.style.transform += `rotate(${this.params.rotate})`)
},
beforeUnmount() {
document.removeEventListener('mouseup', this.touchend, false)
},
methods: {
...mapActions(['updateWidgetData']),
touchstart(e) {
// TODO move start
// const imgKey = e.target.getAttribute('img-key')
// this.editingKey = imgKey
// this.editBoxs[this.editingKey] = {
// transformOrigin: 'center',
// }
// const editBox = this.$refs[this.params.uuid + '_ebox_' + imgKey]
const editBox = this.$refs[this.params.uuid + '_ebox']
this.cropWidgetXY = {
x: Number(editBox.style.left.replace('px', '')) || 0,
y: Number(editBox.style.top.replace('px', '')) || 0,
}
//
document.addEventListener('mousemove', this.handlemousemove, true)
},
touchend() {
//
document.removeEventListener('mousemove', this.handlemousemove, true)
// document.removeEventListener('mouseup', () => {}, true)
},
handlemousemove(e) {
e.stopPropagation()
e.preventDefault()
const { left, top } = this.move(e)
// TODO
this.editBoxStyle.left = left + 'px'
this.editBoxStyle.top = top + 'px'
// this.editBoxs[this.editingKey].left = left + 'px'
// this.editBoxs[this.editingKey].top = top + 'px'
const { width, height } = this.params
const { width: vWidth, height: vHeight } = this.viewBox
const params = {
x: left / (width / vWidth) / this.params.zoom,
y: top / (height / vHeight) / this.params.zoom,
}
// this.svgImg.attr(params)
this.changeFinish('x', params.x)
this.changeFinish('y', params.y)
// console.log('-----', left / (width / vWidth) / this.params.zoom)
},
loadSvg() {
// console.log(this.params)
const _this = this
const Snap = window.Snap
return new Promise((resolve) => {
Snap.load(
this.params.svgUrl,
function (svg) {
let svg2 = Snap(svg.node)
let img = svg2.select('image')
_this.viewBox = svg2.node.viewBox.baseVal
_this.svgImg = img
// TODO
// _this.svgImgs = {}
// svg2.selectAll('image').forEach((element) => {
// element.attr({
// width: '100%',
// height: '100%',
// transform: '',
// 'xlink:href': _this.params.imgs ? _this.params.imgs[element.node.className.baseVal] : '',
// })
// _this.svgImgs[element.node.className.baseVal] = element
// _this.editBoxs[element.node.className.baseVal] = {}
// })
// img.attr({
// width: '100%',
// height: '100%',
// transform: '',
// 'xlink:href': _this.params.imgUrl || '',
// })
const el = this || _this.$refs.widget
el.appendChild(svg.node)
resolve()
},
document.getElementById(this.params.uuid),
)
})
},
updateRecord() {
if (this.dActiveElement.uuid === this.params.uuid) {
let record = this.dActiveElement.record
record.width = this.$refs.widget.offsetWidth
record.height = this.$refs.widget.offsetHeight
}
this.updateZoom()
},
updateZoom() {
// TODO
this.editBoxStyle.transform = `scale(${this.params.zoom})`
// this.editingKey && (this.editBoxs[this.editingKey].transform = `scale(${this.params.zoom})`)
if (this.svgImg) {
const { x, y } = this.params
this.svgImg.attr({
x: x || 0,
y: y || 0,
style: `transform-origin: center;transform: scale(${this.params.zoom})`,
})
// editBox
const { width, height } = this.params
const { width: vWidth, height: vHeight } = this.viewBox
const params = {
left: x * (width / vWidth) * this.params.zoom,
top: y * (height / vHeight) * this.params.zoom,
}
// TODO
this.editBoxStyle.left = params.left + 'px'
this.editBoxStyle.top = params.top + 'px'
// if (this.editingKey) {
// this.editBoxs[this.editingKey].left = params.left + 'px'
// this.editBoxs[this.editingKey].top = params.top + 'px'
// }
}
},
changeFinish(key, value) {
this.updateWidgetData({
uuid: this.params.uuid,
key: key,
value: value,
pushHistory: true,
})
},
move(payload) {
// const widgetXY = { x: this.cropWidgetXY.x / this.dZoom, y: this.cropWidgetXY.y / this.dZoom }
const widgetXY = { x: this.cropWidgetXY.x, y: this.cropWidgetXY.y }
const dx = Number(payload.pageX) - this.dMouseXY.x
const dy = Number(payload.pageY) - this.dMouseXY.y
let left = Number(widgetXY.x) + Math.floor((dx * 100) / this.dZoom)
let top = Number(widgetXY.y) + Math.floor((dy * 100) / this.dZoom)
return { left, top }
},
},
}
</script>
<style lang="less" scoped>
.svg__edit__wrap {
width: 100%;
height: 100%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.w-svg {
cursor: pointer;
outline: none;
}
.edit__model {
opacity: 0.3;
width: 100%;
height: 100%;
object-fit: contain;
}
</style>

View File

@ -1,12 +1,12 @@
<template>
<div class="color__select">
<div class="color__select" :style="{ width }">
<p v-if="label" class="input-label">
{{ label }}
</p>
<div class="content">
<el-popover placement="left-end" width="auto">
<el-popover placement="left-end" trigger="click" width="auto" @after-enter="enter" @before-leave="hide">
<!-- eslint-disable-next-line vue/no-v-model-argument -->
<color-picker v-model:value="innerColor" :modes="['纯色']" @blur="inputBlur" @nativePick="dropColor" />
<color-picker v-model:value="innerColor" :modes="modes" @change="colorChange" @nativePick="dropColor" />
<template #reference>
<div class="color__bar" :style="{ background: innerColor }"></div>
</template>
@ -35,9 +35,15 @@ export default defineComponent({
modelValue: {
default: '',
},
width: {
default: '100%',
},
modes: {
default: () => ['纯色'],
},
},
emits: ['finish', 'update:modelValue'],
setup(props, context) {
emits: ['finish', 'update:modelValue', 'change'],
setup(props, { emit }) {
const store = useStore()
const state: any = reactive({
innerColor: '#ffffffff',
@ -50,7 +56,9 @@ export default defineComponent({
// })
onMounted(() => {
state.innerColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
if (props.modelValue) {
state.innerColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
}
})
const dropColor = async (e: any) => {
console.log('取色: ', e)
@ -75,13 +83,13 @@ export default defineComponent({
)
const updateValue = (value: any) => {
context.emit('update:modelValue', value)
emit('update:modelValue', value)
}
const activeChange = (value: any) => {
updateValue(value)
}
const onChange = () => {
context.emit('finish', state.innerColor)
emit('finish', state.innerColor)
}
// const addHistory = debounce(300, false, async (value) => {
// store.dispatch('pushColorToHistory', value)
@ -94,6 +102,18 @@ export default defineComponent({
state.innerColor = color
}
const enter = () => {
store.commit('setShowMoveable', false) //
}
const hide = () => {
store.commit('setShowMoveable', true) //
}
const colorChange = (e) => {
emit('change', e)
}
return {
...toRefs(state),
// dColorHistory,
@ -101,6 +121,9 @@ export default defineComponent({
onChange,
dropColor,
inputBlur,
enter,
hide,
colorChange,
}
},
})
@ -115,15 +138,16 @@ export default defineComponent({
border-radius: 3px;
width: 100%;
height: 28px;
border: 1px solid rgba(0, 0, 0, 0.1);
// border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: inset 0 0 0 1px rgb(0 0 0 / 6%);
cursor: pointer;
}
&__bar:hover {
border: 1px solid#bdbfc5;
// border: 1px solid #bdbfc5;
box-shadow: inset 0 0 0 1px #bdbfc5;
}
}
.color__select {
width: 100%;
.content {
width: 100%;
align-items: center;

View File

@ -11,7 +11,11 @@
</div>
</div>
</div> -->
<div class="number-input2">
<div v-if="type === 'simple'">
<span class="prepend">{{ prepend }}</span>
<input :class="{ 'small-input': true, disable: !editable }" type="text" :value="modelValue" :readonly="editable ? false : 'readonly'" @input="updateValue($event.target.value)" @focus="focusInput" @blur="blurInput" @keyup="verifyNumber" @keydown="(e) => opNumber(e)" />
</div>
<div v-else class="number-input2">
<div class="input-wrap" @click="edit">
<input :class="{ 'real-input': true, disable: !editable }" type="text" :value="modelValue" :readonly="editable ? false : 'readonly'" @input="updateValue($event.target.value)" @focus="focusInput" @blur="blurInput" @keyup="verifyNumber" @keydown="(e) => opNumber(e)" />
</div>
@ -46,6 +50,9 @@ export default {
default: 1,
},
maxValue: {},
minValue: {},
type: {},
prepend: {},
},
emits: ['finish', 'update:modelValue'],
data() {
@ -73,24 +80,25 @@ export default {
this.updateValue(Number(this.modelValue).toFixed(2))
}, 10)
}
//
//
if (this.maxValue && this.modelValue > this.maxValue) {
setTimeout(() => {
this.updateValue(Number(this.maxValue))
}, 10)
} else if (typeof this.minValue === 'number' && this.modelValue < this.minValue) {
setTimeout(() => {
this.updateValue(Number(this.minValue))
}, 10)
}
},
updateValue(value) {
this.$emit('update:modelValue', value)
this.$emit('update:modelValue', value === '-' ? '-' : Number(value))
},
up() {
this.updateValue(parseInt(this.modelValue || 0, 10) + this.step)
},
down() {
let value = parseInt(this.modelValue || 0, 10) - this.step
if (value < 0) {
value = 0
}
this.updateValue(value)
},
opNumber(e) {
@ -108,9 +116,11 @@ export default {
let value = String(this.modelValue)
let len = value.length
let newValue = ''
for (let i = 0; i < len; ++i) {
let isNegative = value[0] === '-'
// 0
for (let i = isNegative ? 1 : 0; i < len; ++i) {
let c = value[i]
if (c >= '0' && c <= '9') {
if (c == '.' || (c >= '0' && c <= '9')) {
newValue += c
} else {
break
@ -119,13 +129,20 @@ export default {
if (newValue === '') {
newValue = '0'
}
this.updateValue(parseInt(newValue, 10))
if (isNegative) {
newValue = '-' + (newValue === '0' ? '' : newValue)
}
this.updateValue(newValue)
// this.updateValue(parseInt(newValue, 10))
},
focusInput() {
this.inputBorder = true
this.tagText = this.modelValue
},
blurInput() {
if (this.modelValue === '-') {
this.updateValue(0)
}
this.inputBorder = false
if (this.modelValue !== this.tagText) {
this.$emit('finish', this.modelValue)
@ -227,4 +244,20 @@ export default {
width: 100%;
}
}
.small-input {
font-size: 12px;
width: 100%;
padding: 4px 7px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
cursor: text;
}
.prepend {
font-size: 12px;
position: absolute;
margin: -14px 0 0 3px;
transform: scale(0.85);
transform-origin: left;
color: #888888;
}
</style>

View File

@ -2,13 +2,13 @@
* @Author: ShawnPhang
* @Date: 2021-08-09 11:44:29
* @Description: 数值滑块组件
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-29 15:41:38
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-16 09:46:23
-->
<template>
<div id="number-slider">
<span :style="{ width: labelWidth }" class="label">{{ label }}</span>
<el-slider v-model="innerValue" :min="minValue" :max="maxValue" :step="step" input-size="small" show-input :show-tooltip="false" :show-input-controls="false" @change="changeValue"> </el-slider>
<el-slider v-model="innerValue" :min="minValue" :max="maxValue" :step="step" input-size="small" :show-input="showInput" :show-tooltip="false" :show-input-controls="false" @change="changeValue"> </el-slider>
</div>
</template>
@ -32,11 +32,14 @@ export default {
default: 0,
},
maxValue: {
default: 100,
default: 500,
},
step: {
default: 1,
},
showInput: {
default: true,
},
},
emits: ['update:modelValue', 'finish'],
data() {

View File

@ -1,29 +1,16 @@
<!--
* @Author: ShawnPhang
* @Date: 2021-08-02 19:10:06
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-04-07 14:29:42
* @Description: 选项选择未拆分字体选择器
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-20 18:21:55
-->
<template>
<div id="value-select" ref="select" :style="{ width: inputWidth }">
<div ref="select" class="value-select" :style="{ width: inputWidth }">
<p v-if="label" class="input-label">
{{ label }}
</p>
<!-- <div class="input-wrap" :class="{ active: inputBorder }" :style="{ width: inputWidth }">
<input class="real-input" :style="{ textAlign: textAlign }" :class="{ disable: !disable }" :readonly="readonly ? 'readonly' : ''" type="text" :value="showValue" @input="innerValue = $event.target.value.replace(RegExp(suffix), '')" @focus="inputBorder = true" @blur="inputBorder = false" />
<div class="op-btn">
<div class="down" @click="inputBorder = !inputBorder"></div>
</div>
</div>
<el-popover ref="list" v-model="inputBorder" placement="bottom-start" trigger="click" :width="width" popper-class="value-select-list">
<ul v-if="data" class="list-ul">
<li v-for="listItem in data" :key="typeof listItem === 'object' ? listItem.name : listItem" :class="{ active: listItem == innerValue }" @click="selectItem(listItem)">
{{ (typeof listItem === 'object' ? listItem.name : listItem) + suffix }}
</li>
</ul>
</el-popover> -->
<el-popover placement="bottom-end" width="auto">
<el-popover placement="bottom-end" trigger="click" width="auto">
<!-- 单列表 -->
<ul v-if="data && Array.isArray(data)" class="list-ul">
<li v-for="listItem in data" :key="typeof listItem === 'object' ? listItem.alias : listItem" :class="{ active: listItem == innerValue }" @click="selectItem(listItem)">
@ -32,16 +19,18 @@
</li>
</ul>
<!-- tab分类列表 -->
<el-tabs v-else v-model="activeTab">
<el-tab-pane v-for="(val, key, i) in data" :key="'tab' + i" :label="key" :name="key">
<ul class="list-ul">
<li v-for="listItem in data[key]" :key="typeof listItem === 'object' ? listItem.alias : listItem" :class="{ active: listItem == innerValue }" @click="selectItem(listItem)">
<img v-if="listItem.preview" class="preview" :src="listItem.preview" />
<span v-else>{{ (typeof listItem === 'object' ? listItem.alias : listItem) + suffix }}</span>
</li>
</ul>
</el-tab-pane>
</el-tabs>
<div v-else class="tabs-wrap">
<el-tabs v-model="activeTab">
<el-tab-pane v-for="(val, key, i) in data" :key="'tab' + i" :label="key" :name="key">
<ul class="list-ul">
<li v-for="listItem in data[key]" :key="typeof listItem === 'object' ? listItem.alias : listItem" :class="{ active: listItem == innerValue }" @click="selectItem(listItem)">
<img v-if="listItem.preview" class="preview" :src="listItem.preview" />
<span v-else :style="{ fontFamily: `'${listItem.value}'` }">{{ (typeof listItem === 'object' ? listItem.alias : listItem) + suffix }}</span>
</li>
</ul>
</el-tab-pane>
</el-tabs>
</div>
<template #reference>
<div :class="['input-wrap', { active: inputBorder }]" :style="{ width: inputWidth }">
<!-- <img v-if="innerPreview" class="preview" :src="innerPreview" /> -->
@ -190,7 +179,7 @@ export default {
@color0: #e1e1e1; // Appears 2 times
@color1: #d1d1d1; // Appears 2 times
#value-select {
.value-select {
// height: 60px;
line-height: 1.15;
width: 80px;
@ -298,4 +287,8 @@ export default {
height: 1.6em;
}
}
.tabs-wrap {
width: 210px;
}
</style>

View File

@ -1,6 +1,12 @@
<template>
<div id="page-style">
<el-collapse v-model="activeNames">
<div v-if="showBgLib" style="width: 256px;height: 100%;">
<span class="header-back" @click="showBgLib = false">
<i class="iconfont icon-right"></i> 选择背景
</span>
<bg-img-list-wrap style="padding-top: 2rem;" model="stylePanel" />
</div>
<el-collapse v-else v-model="activeNames">
<el-collapse-item title="画布尺寸" name="1">
<div class="position-size">
<number-input v-model="innerElement.width" label="宽" :maxValue="5000" @finish="(value) => finish('width', value)" />
@ -8,23 +14,22 @@
</div>
</el-collapse-item>
<el-collapse-item title="背景设置" name="2">
<el-button style="width: 100%; margin: 0 0 1rem 0;" type="primary" link @click="showBgLib = true">在背景库中选择</el-button>
<Tabs :value="mode" @update:value="onChangeMode">
<TabPanel v-for="label in modes" :key="label" :label="label"></TabPanel>
</Tabs>
<color-select v-show="mode === '颜色'" v-model="innerElement.backgroundColor" @finish="(value) => finish('backgroundColor', value)" />
<color-select v-show="mode === '颜色'" v-model="innerElement.backgroundColor" :modes="['纯色']" @change="colorChange" @finish="(value) => finish('backgroundColor', value)" />
<!-- <bg-img-select :img="innerElement.backgroundImage"/> -->
<div v-if="mode === '图片' && innerElement.backgroundImage" style="margin-top: 2rem">
<el-image style="max-height: 428px" :src="innerElement.backgroundImage" fit="contain"></el-image>
<el-button style="width: 100%; margin-top: 0.7rem" size="small" @click="deleteBg">删除</el-button>
<el-button class="btn-wrap" size="small" @click="deleteBg">删除</el-button>
</div>
<uploader v-show="mode === '图片'" style="width: 100%; margin-top: 0.7rem" @done="uploadImgDone">
<el-button style="width: 100%" plain>{{ innerElement.backgroundImage ? '换背景' : '上传背景' }}</el-button>
<uploader v-show="mode === '图片'" class="btn-wrap" @done="uploadImgDone">
<el-button style="width: 100%" plain>{{ innerElement.backgroundImage ? '换背景' : '上传背景' }}</el-button>
</uploader>
<el-button v-show="mode === '图片' && innerElement.backgroundImage" style="width: 100%; margin-top: 0.7rem" size="small" @click="downloadBG">{{ downP ? downP + ' %' : '下载背景图' }}</el-button>
<el-button v-show="mode === '图片' && innerElement.backgroundImage" class="btn-wrap" size="small" @click="downloadBG">{{ downP ? downP + ' %' : '下载背景图' }}</el-button>
</el-collapse-item>
<!-- <el-collapse-item title="其他设置" name="3">
<el-input v-model="innerElement.name" label="名称" @finish="(value) => finish('name', value)" />
</el-collapse-item> -->
</el-collapse>
</div>
</template>
@ -52,9 +57,9 @@ export default {
tag: false,
ingoreKeys: ['backgroundColor', 'name', 'width', 'height'],
downP: 0,
// canvasRunning: false,
mode: '颜色',
modes: ['颜色', '图片'],
showBgLib: false
}
},
computed: {
@ -79,6 +84,14 @@ export default {
},
methods: {
...mapActions(['updatePageData']),
colorChange(e) {
if (e.mode === '渐变') {
// setTimeout(() => {
// console.log(1, e)
// this.finish('backgroundImage', e.color)
// }, 1000)
}
},
onChangeMode(value) {
this.mode = value
if (value === '颜色') {
@ -181,4 +194,25 @@ export default {
.select {
margin-bottom: 10px;
}
.btn-wrap {
width: 100%; margin-top: 0.7rem;
}
.header {
&-back {
cursor: pointer;
display: flex;
align-items: center;
color: #333;
font-size: 14px;
font-weight: 600;
height: 2.9rem;
position: absolute;
z-index: 2;
background: #ffffff;
width: 259px;
.icon-right {
transform: rotate(180deg);
}
}
}
</style>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-02 09:41:41
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-03-15 10:22:14
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-16 00:30:03
-->
<template>
<div
@ -144,7 +144,6 @@ export default {
// const opacity = this.$refs.widget.style.opacity
// this.$refs.widget.style.opacity = 1
setTimeout(() => {
console.log(this.temp)
if (!this.temp) {
return
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-09 11:41:53
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-12 12:50:39
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 00:59:44
-->
<template>
<div id="w-image-style">
@ -21,12 +21,12 @@
<el-button style="width: 100%; margin-bottom: 12px" plain @click="openPicBox">替换图片</el-button>
<div class="options">
<el-button v-if="innerElement.cropEdit" plain type="primary" @click="imgCrop(false)">完成</el-button>
<el-button v-else plain type="primary" @click="imgCrop(true)"><i class="icon sd-caijian" />裁剪</el-button>
<el-button @click="openImageCutout" plain>抠图</el-button>
<el-button v-else plain type="primary" @click="imgCrop(true)"><i class="icon sd-caijian" /> 裁剪</el-button>
<el-button plain @click="openImageCutout"><i class="icon sd-AIkoutu" /> 抠图</el-button>
<!-- <uploader class="options__upload" @done="uploadImgDone">
<el-button size="small" plain>替换图片</el-button>
</uploader> -->
<el-button size="small" disabled plain @click="openCropper">图片美化</el-button>
<el-button size="small" disabled plain @click="openCropper">美化</el-button>
</div>
<!-- <container-wrap @change="changeContainer" />
<br /> -->
@ -52,7 +52,7 @@
<i style="padding: 0 8px; cursor: pointer" class="icon sd-queren" @click="imgCrop(false)" />
</inner-tool-bar>
<picBox ref="picBox" @select="selectDone" />
<imageCutout ref="imageCutout" />
<imageCutout ref="imageCutout" @done="cutImageDone" />
</div>
</template>
@ -241,8 +241,23 @@ export default {
openPicBox() {
this.$refs.picBox.open()
},
//
openImageCutout() {
this.$refs.imageCutout.open()
fetch(this.innerElement.imgUrl)
.then((response) => response.blob())
.then((blob) => {
const file = new File([blob], `image_${Math.random()}.jpg`, { type: 'image/jpeg' })
this.$refs.imageCutout.open(file)
})
.catch((error) => {
console.error('获取图片失败:', error)
})
},
//
async cutImageDone(url) {
setTimeout(() => {
this.innerElement.imgUrl = url
}, 300)
},
},
}
@ -277,6 +292,9 @@ export default {
margin-left: 10px;
display: inline-block;
}
.icon {
margin-right: 0.3em;
}
}
.slide-wrap {

View File

@ -0,0 +1,26 @@
/*
* @Author: ShawnPhang
* @Date: 2023-11-29 11:00:41
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-29 11:01:50
*/
export default (effect: any) => {
let result = ''
switch (Number(effect.filling.type)) {
case 2:
{
const { angle, stops } = effect.filling.gradient
const gradients = stops.map((x: any) => `${x.color} ${Number(x.offset) * 100}%`)
result = `linear-gradient(${angle}deg, ${gradients.toString()})`
}
break
case 1:
result = `url(${effect.filling.imageContent.image})`
break
default:
result = effect.filling.color
break
}
return result
}

View File

@ -0,0 +1,22 @@
/*
* @Author: ShawnPhang
* @Date: 2023-10-14 20:16:48
* @Description: 使
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-14 20:29:26
*/
import store from '@/store'
import { toRaw } from 'vue'
export default () => {
const collector = new Set()
const fonts: any = {}
const { dWidgets: widgets } = store.getters
for (let i = 0; i < widgets.length; i++) {
const { type, fontClass } = widgets[i]
if (type === 'w-text') {
collector.add(fontClass.id)
fonts[fontClass.id] = toRaw(fontClass)
}
}
return Array.from(collector).map((id: any) => fonts[id])
}

View File

@ -33,11 +33,12 @@
:key="efi + 'effect'"
:style="{
fontFamily: `'${params.fontClass.value}'`,
color: ef.filling && ef.filling.type === 0 ? ef.filling.color : 'transparent',
webkitTextStroke: ef.stroke ? `${ef.stroke.width}px ${ef.stroke.color}` : undefined,
textShadow: ef.shadow ? `${ef.shadow.offsetX}px ${ef.shadow.offsetY}px ${ef.shadow.blur}px ${ef.shadow.color}` : undefined,
backgroundImage: ef.filling ? (ef.filling.type === 0 ? undefined : getGradientOrImg(ef)) : undefined,
webkitBackgroundClip: ef.filling ? (ef.filling.type === 0 ? undefined : 'text') : undefined,
color: ef.filling && ef.filling.enable && ef.filling.type === 0 ? ef.filling.color : 'transparent',
webkitTextStroke: ef.stroke && ef.stroke.enable ? `${ef.stroke.width}px ${ef.stroke.color}` : undefined,
textShadow: ef.shadow && ef.shadow.enable ? `${ef.shadow.offsetX}px ${ef.shadow.offsetY}px ${ef.shadow.blur}px ${ef.shadow.color}` : undefined,
backgroundImage: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : getGradientOrImg(ef)) : undefined,
webkitBackgroundClip: ef.filling && ef.filling.enable ? (ef.filling.type === 0 ? undefined : 'text') : undefined,
transform: ef.offset && ef.offset.enable ? `translate(${ef.offset.x}px, ${ef.offset.y}px)` : undefined,
}"
class="edit-text effect-text"
spellcheck="false"
@ -54,6 +55,7 @@ const NAME = 'w-text'
import { mapGetters, mapActions } from 'vuex'
import { fontWithDraw } from '@/utils/widgets/loadFontRule'
import getGradientOrImg from './getGradientOrImg.ts'
export default {
name: NAME,
@ -70,10 +72,10 @@ export default {
fontSize: 24,
zoom: 1,
fontClass: {
alias: '素材集市酷方体',
id: 33925853,
value: 'sucaijishikufangti',
url: 'https://res.palxp.com/static/fonts/20200809-152508-8654.woff',
alias: '站酷快乐体',
id: 543,
value: 'zcool-kuaile-regular',
url: 'https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2',
},
fontFamily: 'SourceHanSansSC-Regular',
fontWeight: 'normal',
@ -120,10 +122,12 @@ export default {
if (font.url && !isDone) {
if (font.id && this.isDraw) {
// url
// demobug
this.loading = false
return
}
this.loading = true
this.loading = !this.isDraw
const loadFont = new window.FontFace(font.value, `url(${font.url})`)
await loadFont.load()
document.fonts.add(loadFont)
@ -151,34 +155,13 @@ export default {
async mounted() {
this.updateRecord()
// await this.$nextTick()
this.styleEffect()
this.params.transform && (this.$refs.widget.style.transform = this.params.transform)
this.params.rotate && (this.$refs.widget.style.transform += `translate(0px, 0px) rotate(${this.params.rotate}) scale(1, 1)`)
// this.$store.commit('updateRect')
},
methods: {
...mapActions(['updateWidgetData', 'pushHistory']),
getGradientOrImg(ef) {
return ef.filling.type === 2 ? `linear-gradient(${ef.filling.gradient.angle}deg, ${ef.filling.gradient.stops[0].color} ${Number(ef.filling.gradient.stops[0].offset) * 100}%, ${ef.filling.gradient.stops[1].color} ${Number(ef.filling.gradient.stops[1].offset) * 100}%)` : `url(${ef.filling.imageContent.image})`
},
styleEffect() {
if (this.params.textEffects && this.params.textEffects.length > 0) {
const style = this.params.textEffects[this.params.textEffects.length - 1]
if (style.filling && style.filling.color && style.filling.type == 0) {
this.params.color = style.filling.color
}
}
// if (this.params.textEffects && this.params.textEffects.length <= 2 && this.params.textEffects[0]) {
// //
// const grad = this.params.textEffects[0].type == 1 && this.params.textEffects[0].filling?.gradient
// if (grad) {
// this.$refs.widget.style['-webkit-background-clip'] = 'text' // background-clip: text;
// // this.$refs.widget.style['-webkit-text-fill-color'] = 'transparent' // -webkit-text-fill-color: transparent;
// // // this.$refs.widget.style.backgroundImage = `url('https://st-gdx.dancf.com/gaodingx/0/personal/my-tasks/20210827-183407-abd3.jpg?x-oss-process=image/resize,w_176,h_176,m_fill/interlace,1,image/format,webp')`
// this.$refs.widget.style.backgroundImage = `linear-gradient(${grad.angle}deg, ${grad.stops[0].color} ${Number(grad.stops[0].offset) * 100}%, ${grad.stops[1].color} ${Number(grad.stops[1].offset) * 100}%)`
// }
// }
},
getGradientOrImg,
updateRecord() {
if (this.dActiveElement.uuid === this.params.uuid) {
let record = this.dActiveElement.record
@ -212,7 +195,6 @@ export default {
pushHistory: false,
})
this.$store.commit('updateRect')
// this.styleEffect()
},
writeDone(e) {
this.editable = false

View File

@ -24,7 +24,7 @@
</div> -->
<!-- <el-collapse-item title="位置尺寸" name="1"> -->
<div class="style-item slide-wrap">
<number-slider v-model="innerElement.letterSpacing" style="font-size: 14px" label="字距" labelWidth="40px" :step="0.05" :minValue="-50" :maxValue="innerElement.fontSize" @finish="(value) => finish('letterSpacing', value)" />
<number-slider v-model="innerElement.letterSpacing" style="font-size: 14px" label="字距" labelWidth="40px" :step="0.05" :minValue="-innerElement.fontSize" :maxValue="innerElement.fontSize * 2" @finish="(value) => finish('letterSpacing', value)" />
<number-slider v-model="innerElement.lineHeight" style="font-size: 14px" label="行距" labelWidth="40px" :step="0.05" :minValue="0" :maxValue="2.5" @finish="(value) => finish('lineHeight', value)" />
</div>
<!-- </el-collapse-item> -->
@ -34,7 +34,7 @@
<!-- <color-select v-model="innerElement.backgroundColor" label="背景颜色" @finish="(value) => finish('backgroundColor', value)" /> -->
</div>
<div class="line-layout style-item">
<effect-wrap v-model="innerElement.isEffect" :data="innerElement.textEffects" :degree="innerElement.degree" @select="testEffect" />
<effect-wrap v-model="innerElement.textEffects" :data="innerElement" :degree="innerElement.degree" @select="testEffect" />
</div>
<icon-item-select class="style-item" :data="layerIconList" @finish="layerAction" />
<icon-item-select class="style-item" :data="alignIconList" @finish="alignAction" />
@ -51,8 +51,8 @@
<script>
//
const NAME = 'w-text-style'
import api from '@/api'
import _config from '@/config'
// import api from '@/api'
// import _config from '@/config'
import { mapGetters, mapActions } from 'vuex'
import { styleIconList1, styleIconList2, alignIconList } from '../../../../assets/data/TextIconsData'
import layerIconList from '@/assets/data/LayerIconList'
@ -64,6 +64,7 @@ import textInputArea from '../../settings/textInputArea.vue'
import valueSelect from '../../settings/valueSelect.vue'
import effectWrap from '../../settings/EffectSelect/TextWrap.vue'
import { useFontStore } from '@/common/methods/fonts'
import usePageFontsFilter from './pageFontsFilter.ts'
export default {
name: NAME,
@ -114,6 +115,7 @@ export default {
methods: {
...mapActions(['updateWidgetData', 'updateAlign', 'updateLayerIndex', 'pushHistory']),
testEffect({ key, value, style }) {
console.log('选择回调')
const uuid = this.dActiveElement.uuid
this.$store.commit('setWidgetStyle', { uuid, key, value })
if (style) {
@ -124,11 +126,13 @@ export default {
// if (!this.isDraw) {
// useFontStore().init()
const localFonts = useFontStore.list
const fontLists = { 中文: [], 英文: [] }
const fontLists = { 当前页面: [], 中文: [], 英文: [] }
for (const font of localFonts) {
const { id, value, url, alias, preview, lang } = font
lang === 'zh' ? fontLists['中文'].unshift({ id, value, url, alias, preview }) : fontLists['英文'].unshift({ id, value, url, alias, preview })
const { id, oid, value, url, alias, preview, lang } = font
const item = { id, oid, value, url, alias, preview }
lang === 'zh' ? fontLists['中文'].unshift(item) : fontLists['英文'].unshift(item)
}
fontLists['当前页面'] = usePageFontsFilter()
this.fontClassList = fontLists
// }
// const isDev = process.env.NODE_ENV === 'development'
@ -180,6 +184,9 @@ export default {
value: value,
pushHistory: false,
})
setTimeout(() => {
key === 'fontClass' && (this.fontClassList['当前页面'] = usePageFontsFilter())
}, 300)
},
layerAction(item) {
this.updateLayerIndex({

View File

@ -1,28 +1,22 @@
/*
* @Author: ShawnPhang
* @Date: 2023-09-07 22:56:09
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-16 20:39:29
*/
// const prefix = import.meta.env
const prefix = process.env
const isDev = prefix.NODE_ENV === 'development'
import { version } from '../package.json'
export default {
isDev,
BASE_URL: isDev ? '/' : './',
VERSION: '1.1.0',
VERSION: version,
APP_NAME: '迅排设计',
COPYRIGHT: 'ShawnPhang - Palxp.cn',
// API_URL: isDev ? 'http://localhost:9998' : '${API}',
API_URL: 'https://palxp.cn:8887', // 服务端地址
SCREEN_URL: isDev ? 'http://localhost:7001' : '#{SCREEN_URL}', // 截图服务地址
IMG_URL: 'https://store.palxp.com/', // 七牛云资源地址
KT_URL: 'https://res.palxp.cn:5001', // 抠图服务地址
// ICONFONT_URL: '//at.alicdn.com/t/font_3223711_74mlzj4jdue.css',
ICONFONT_URL: '//at.alicdn.com/t/font_2717063_ypy8vprc3b.css?display=swap',
ICONFONT_EXTRA: '//at.alicdn.com/t/c/font_3228074_6qsac4kteu7.css?&display=swap',
ICONFONT_EXTRA: '//at.alicdn.com/t/c/font_3228074_42xym3extur.css',
QINIUYUN_PLUGIN: 'https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/qiniu-js/2.5.5/qiniu.min.js',
supportSubFont: true, // 是否开启服务端字体压缩
}

8
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
// / <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -2,10 +2,11 @@
* @Author: ShawnPhang
* @Date: 2022-03-09 16:29:54
* @Description: ctrl建相关的操作
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-03-25 16:12:27
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 09:49:54
*/
import store from '@/store'
import handlePaste from './handlePaste'
export default function dealWithCtrl(e: any, _this: any) {
switch (e.keyCode) {
@ -63,6 +64,7 @@ function copy() {
*
*/
function paste() {
handlePaste()
if (store.getters.dCopyElement.length === 0) {
return
} else if (store.getters.dActiveElement.isContainer && checkGroupChild(store.getters.dActiveElement.uuid, 'editable')) {

View File

@ -0,0 +1,60 @@
/*
* @Author: ShawnPhang
* @Date: 2023-10-09 09:47:40
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 10:35:21
*/
// window.addEventListener('paste', (e: any) => {
// const clipdata = e.clipboardData || (window as any).clipboardData
// console.log('主动粘贴', clipdata.getData('text/plain'))
// })
import store from '@/store'
import api from '@/api'
import Qiniu from '@/common/methods/QiNiu'
import _config from '@/config'
import { getImage } from '@/common/methods/getImgDetail'
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
import wText from '@/components/modules/widgets/wText/wText.vue'
export default () => {
navigator.clipboard
.read()
.then(async (dataTransfer: any) => {
for (let i = 0; i < dataTransfer.length; i++) {
const item = dataTransfer[i]
if (item.types.toString().indexOf('image') !== -1) {
const imageBlob = await item.getType(item.types[0])
const file = new File([imageBlob], 'screenshot.png', { type: 'image/png' })
// 上传图片
const qnOptions = { bucket: 'xp-design', prePath: 'user' }
const result: any = await Qiniu.upload(file, qnOptions)
const { width, height }: any = await getImage(file)
const url = _config.IMG_URL + result.key
await api.material.addMyPhoto({ width, height, url })
// 添加图片到画布中
store.commit('setShowMoveable', false) // 清理掉上一次的选择
const setting = JSON.parse(JSON.stringify(wImage.setting))
setting.width = width
setting.height = height
setting.imgUrl = url
const { width: pW, height: pH } = store.getters.dPage
setting.left = pW / 2 - width / 2
setting.top = pH / 2 - height / 2
store.dispatch('addWidget', setting)
break
} else if (item.types.toString().indexOf('text') !== -1) {
store.commit('setShowMoveable', false) // 清理掉上一次的选择
const setting = JSON.parse(JSON.stringify(wText.setting))
setting.text = await navigator.clipboard.readText()
store.dispatch('addWidget', setting)
break
}
}
})
.catch((error) => {
console.error('无法读取剪贴板内容:', error)
})
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-03-09 14:20:09
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-30 17:29:28
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-30 10:09:55
*/
import store from '@/store'
@ -24,12 +24,19 @@ export default function keyCodeOptions(e: any, params: any) {
break
case 46:
case 8:
if (store.getters.dActiveElement.isContainer) {
if (checkGroupChild(store.getters.dActiveElement.uuid, 'editable')) {
return
{
if (store.getters.dActiveElement.isContainer) {
if (checkGroupChild(store.getters.dActiveElement.uuid, 'editable')) {
return
}
}
const { type, editable }: any = store.getters.dActiveElement
if (type === 'w-text') {
// 不在编辑状态则执行删除
!editable && store.getters.showMoveable && store.dispatch('deleteWidget')
} else store.dispatch('deleteWidget')
}
!store.getters.dActiveElement.editable && store.dispatch('deleteWidget')
break
}
}

View File

@ -3,7 +3,7 @@
* @Date: 2021-08-01 14:12:08
* @Description: mixin形式放入views/index.vue中
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-11 23:37:26
* @LastEditTime: 2023-09-19 17:29:06
*/
import store from '@/store'
const _this: any = {}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-19 18:43:22
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-25 17:00:52
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:32:04
*/
export default [
// {

View File

@ -3,11 +3,10 @@
* @Date: 2021-12-16 16:20:16
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-15 12:47:57
* @LastEditTime: 2023-09-28 17:42:25
*/
import mutations from './mutations'
import actions from './actions'
import { client } from '@gradio/client'
import _config from '@/config'
const all = {
@ -39,10 +38,6 @@ const all = {
fonts: (state: Type.Object) => {
return state.fonts
},
app: async (state: Type.Object) => {
!state.app && (state.app = await client(_config.KT_URL))
return state.app
},
},
mutations: {
...mutations,

View File

@ -623,8 +623,8 @@ export default {
const group = JSON.parse(store.state.dGroupJson)
group.uuid = nanoid()
widgets.push(group)
let left = store.state.dPage.width
let top = store.state.dPage.height
let left = Number(store.state.dPage.width)
let top = Number(store.state.dPage.height)
let right = 0
let bottom = 0
const sortWidgets = [] // 顺序取出元素
@ -672,11 +672,10 @@ export default {
// widgets.push(sortWidgets[i].widget)
// }
group.left = left
group.top = top
group.width = right - left
group.height = bottom - top
group.left = Number(left)
group.top = Number(top)
group.width = Number(right - left)
group.height = Number(bottom - top)
store.state.dActiveElement = group
store.state.dSelectWidgets = []

View File

@ -1,16 +1,16 @@
/*
* @Author: ShawnPhang
* @Date: 2021-07-13 02:48:38
* @Description: code为200时返回result结果对象
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-31 09:34:34
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-11 17:36:33
*/
import axios from 'axios'
import store from '@/store'
import app_config from '@/config'
axios.defaults.timeout = 30000
// axios.defaults.headers.Authorization = 'Bearer ';
axios.defaults.headers.authorization = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAwMDEsImV4cCI6MTc4ODU3NDc1MDU4NX0.L_t6DFD48Dm6rUPfgIgOWJkz18En1m_-hhMHcpbxliY';
// const version = app_config.VERSION;
const baseUrl = app_config.API_URL
@ -48,14 +48,11 @@ axios.interceptors.response.use(
(res: Type.Object) => {
// store.dispatch('hideLoading');
// if (res.status !== 200) {
// // return falseInfo
// }
// 接口规则只有正确code为200时返回result结果对象错误返回整个结果对象
if (!res.data) {
return Promise.reject(res)
}
// console.log(res.headers.authorization);
if (res.data.code === 401) {
console.log('登录失效')
store.commit('changeOnline', false)
@ -71,10 +68,6 @@ axios.interceptors.response.use(
},
(error) => {
// if (error.response.status === 401) {
// setTimeout(() => {
// window.localStorage.clear()
// // window.location.href = app_config.login + "?" + "redirect=" + window.location.href;
// }, 1000)
// }
store.dispatch('hideLoading')
return Promise.reject(error)
@ -89,21 +82,19 @@ const fetch = (url: string, params: Type.Object, type: string | undefined = 'get
// store.commit('loading', '加载中..');
}
const objtest: Type.Object = {}
// const { value } = JSON.parse(localStorage.getItem('pro__Access-Token') || '{}')
// objtest.Authorization = `Bearer ${value}`
const token = localStorage.getItem('xp_token')
const headerObject: Type.Object = { }
token && (headerObject.authorization = token)
if (type === 'get') {
return axios.get(url, {
// headers: {
// // Authorization: String(localStorage.getItem('token')),
// },
headers: Object.assign(objtest, exheaders),
headers: Object.assign(headerObject, exheaders),
params,
...extra,
})
} else {
return (axios as Type.Object)[type](url, params, {
headers: Object.assign(objtest, exheaders),
headers: Object.assign(headerObject, exheaders),
...extra,
})
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-03-06 13:53:30
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-17 14:24:39
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:32:40
*/
export default class PointImg {
private canvas: any

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-12-24 15:13:58
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-17 14:25:53
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:19:07
*/
export default class PreLoad {
private i: number

View File

@ -27,7 +27,7 @@ import {
// ElContainer,
// ElDatePicker,
ElDialog,
// ElDivider,
ElDivider,
// ElDrawer,
// ElDropdown,
// ElDropdownItem,
@ -115,7 +115,7 @@ const components = [
// ElContainer,
// ElDatePicker,
ElDialog,
// ElDivider,
ElDivider,
// ElDrawer,
// ElDropdown,
// ElDropdownItem,

View File

@ -1,29 +1,34 @@
/*
* @Author: ShawnPhang
* @Date: 2023-08-23 17:37:16
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-08-23 17:48:34
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-14 18:31:29
*/
/**
* ttf/otf这种原始字体支持提取false
*/
import _config from '@/config'
export const fontWithDraw = _config.supportSubFont // true 开启false 关闭
import api from '@/api'
import { blob2Base64, generateFontStyle } from '@/common/methods/fonts/utils'
export const fontWithDraw = true // true开启false关闭
export const font2style = async (fontContent: any, fontData: any = []) => {
return new Promise((resolve: Function) => {
Promise.all(
// 提取字体子集。只有ttf/otf这种原始字体支持提取如果服务端不支持则关闭该功能以保证页面能加载字体。
Object.keys(fontContent).map(async (key) => {
const font = fontData.find((font: any) => font.value === key) as any
if (font.id) {
const extra = font.oid ? {} : { responseType: 'blob' }
const params = {
font_id: font.oid,
id: font.id,
content: shortText(fontContent[key]),
}
try {
const base64 = await api.material.getFontSub({
font_id: font.id,
url: font.url,
content: 'Aa' + fontContent[key],
})
fontContent[key] = base64
const result = await api.material.getFontSub(params, extra)
fontContent[key] = font.oid ? result : await blob2Base64(result)
} catch (e) {
console.log('字体获取失败', e)
}
@ -37,3 +42,9 @@ export const font2style = async (fontContent: any, fontData: any = []) => {
})
})
}
function shortText(text: string) {
// 文字去重
const textArr = Array.from(new Set(text.split('')))
return textArr.join('')
}

View File

@ -10,7 +10,7 @@
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { mapActions } from 'vuex'
import { mapActions, mapGetters } from 'vuex'
import api from '@/api'
import wGroup from '@/components/modules/widgets/wGroup/wGroup.vue'
import Preload from '@/utils/plugins/preload'
@ -33,6 +33,9 @@ export default defineComponent({
...toRefs(state),
}
},
computed: {
...mapGetters(['dPage']),
},
mounted() {
this.initGroupJson(JSON.stringify(wGroup.setting))
this.$nextTick(() => {
@ -40,16 +43,25 @@ export default defineComponent({
})
},
methods: {
...mapActions(['initGroupJson', 'setTemplate']),
...mapActions(['initGroupJson', 'setTemplate', 'addGroup']),
async load() {
let loadFlag = false
const { id, tempid } = this.$route.query
const { id, tempid, tempType: type } = this.$route.query
if (id || tempid) {
const { data } = await api.home[id ? 'getWorks' : 'getTempDetail']({ id: id || tempid })
const { data, width, height } = await api.home[id ? 'getWorks' : 'getTempDetail']({ id: id || tempid, type })
const content = JSON.parse(data)
const widgets = type == 1 ? content : content.widgets
if (type == 1) {
this.dPage.width = width
this.dPage.height = height
this.dPage.backgroundColor = '#ffffff00'
this.addGroup(content)
} else {
this.$store.commit('setDPage', content.page)
id ? this.$store.commit('setDWidgets', widgets) : this.setTemplate(widgets)
}
this.$store.commit('setDPage', content.page)
id ? this.$store.commit('setDWidgets', content.widgets) : this.setTemplate(content.widgets)
await this.$nextTick()
const imgsData: any = []
@ -57,7 +69,7 @@ export default defineComponent({
const fontLoaders: any = []
const fontContent: any = {}
let fontData: any = []
content.widgets.forEach((item: any) => {
widgets.forEach((item: any) => {
if (item.fontClass && item.fontClass.value) {
const loader = new FontFaceObserver(item.fontClass.value)
fontData.push(item.fontClass)
@ -71,29 +83,25 @@ export default defineComponent({
}
// svg
try {
if (item.imgUrl && !item.isNinePatch) {
if (item.svgUrl && item.type === 'w-svg') {
const cNodes: any = (window as any).document.getElementById(item.uuid).childNodes
svgsData.push(cNodes)
} else if (item.imgUrl && !item.isNinePatch) {
const cNodes: any = (window as any).document.getElementById(item.uuid).childNodes
for (const el of cNodes) {
if (el.className && el.className.includes('img__box')) {
imgsData.push(el.firstChild)
}
}
} else if (item.svgUrl) {
const cNodes: any = (window as any).document.getElementById(item.uuid).childNodes
svgsData.push(cNodes)
}
} catch (e) {}
})
// TODO:
if (content.page.backgroundImage) {
if (content.page?.backgroundImage) {
const preloadBg = new Preload([content.page.backgroundImage])
await preloadBg.imgs()
}
try {
const { list } = await api.material.getFonts({ ids: fontData.map((x: any) => x.id) })
fontData.forEach((item: any) => {
item.url = list.find((x: any) => x.oid == item.id)?.ttf
})
fontWithDraw && (await font2style(fontContent, fontData))
// console.log('1. base64 yes')
const preload = new Preload(imgsData)
@ -138,3 +146,9 @@ export default defineComponent({
}
}
</style>
<style lang="less">
.layer-hover {
outline: 0 !important;
}
</style>

View File

@ -1,3 +1,10 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-09-18 17:34:44
* @Description:
* @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-02-25 14:51:00
-->
<template>
<div id="page-design-index" ref="pageDesignIndex" class="page-design-bg-color">
<div :style="style" class="top-nav">
@ -18,10 +25,10 @@
<div class="page-design-index-wrap">
<widget-panel></widget-panel>
<design-board class="page-design-wrap" pageDesignCanvasId="page-design-canvas">
<!-- 用于挡住画布溢出部分因为使用overflow有bug. PS:如shadow没有透明度则可以完全遮挡元素 -->
<!-- 用于挡住画布溢出部分因为使用overflow有bug -->
<div class="shelter" :style="{ width: Math.floor((dPage.width * dZoom) / 100) + 'px', height: Math.floor((dPage.height * dZoom) / 100) + 'px' }"></div>
<!-- 提供一个背景图层以免遮挡穿帮 -->
<div class="shelter-bg" :style="{ width: Math.floor((dPage.width * dZoom) / 100) + 'px', height: Math.floor((dPage.height * dZoom) / 100) + 'px' }"></div>
<!-- 提供一个背景图层 -->
<div class="shelter-bg transparent-bg" :style="{ width: Math.floor((dPage.width * dZoom) / 100) + 'px', height: Math.floor((dPage.height * dZoom) / 100) + 'px' }"></div>
</design-board>
<style-panel></style-panel>
</div>
@ -34,7 +41,13 @@
<!-- 旋转缩放组件 -->
<Moveable />
<!-- 遮罩百分比进度条 -->
<ProgressLoading :percent="downloadPercent" :text="downloadText" cancelText="取消" @cancel="downloadCancel" @done="downloadPercent = 0" />
<ProgressLoading
:percent="downloadPercent"
:text="downloadText"
cancelText="取消"
@cancel="downloadCancel"
@done="downloadPercent = 0"
/>
</div>
</template>
@ -69,7 +82,7 @@ export default defineComponent({
zoomControl,
lineGuides,
},
mixins: [shortcuts],
// mixins: [shortcuts],
setup() {
!_config.isDev && window.addEventListener('beforeunload', beforeUnload)
@ -129,6 +142,7 @@ export default defineComponent({
},
methods: {
...mapActions(['selectWidget', 'initGroupJson', 'handleHistory']),
...shortcuts.methods,
changeLineGuides() {
this.showLineGuides = !this.showLineGuides
},
@ -137,8 +151,9 @@ export default defineComponent({
this.isContinue = false
},
loadData() {
const { id, tempid } = this.$route.query
;(this.$refs as any).options.load(id, tempid, async () => {
//
const { id, tempid, tempType } = this.$route.query
;(this.$refs as any).options.load(id, tempid, tempType, async () => {
;(this.$refs as any).zoomControl.screenChange()
await this.$nextTick()
// page
@ -160,7 +175,7 @@ export default defineComponent({
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
this.style.left = `-${scrollLeft}px`
},
clickListener(e) {
clickListener(e: Event) {
console.log('click listener', e)
},
optionsChange({ downloadPercent, downloadText }: any) {

View File

@ -3,7 +3,7 @@
* @Date: 2022-01-10 14:57:53
* @Description: Psd文件解析
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-15 12:57:02
* @LastEditTime: 2023-10-13 00:12:11
-->
<template>
<div id="page-design-index" ref="pageDesignIndex">
@ -56,7 +56,7 @@ import designBoard from '@/components/modules/layout/designBoard.vue'
import zoomControl from '@/components/modules/layout/zoomControl.vue'
import HeaderOptions from './components/UploadTemplate.vue'
import ProgressLoading from '@/components/common/ProgressLoading/index.vue'
import MyWorker from '@/utils/plugins/webWorker'
// import MyWorker from '@/utils/plugins/webWorker'
import { processPSD2Page } from '@/utils/plugins/psd'
export default defineComponent({

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-01-12 11:26:53
* @Description: 顶部操作按钮组
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-08-23 17:33:12
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 12:40:59
-->
<template>
<div class="top-title"><el-input v-model="title" placeholder="未命名的设计" class="input-wrap" /></div>
@ -36,6 +36,8 @@ import SaveImage from '@/components/business/save-download/CreateCover.vue'
import { useFontStore } from '@/common/methods/fonts'
import copyRight from './CopyRight.vue'
import _config from '@/config'
import useConfirm from '@/common/methods/confirm'
import wGroup from '@/components/modules/widgets/wGroup/wGroup.vue'
export default defineComponent({
components: { copyRight, SaveImage },
@ -70,20 +72,43 @@ export default defineComponent({
}
//
async function saveTemp() {
const { tempid } = route.query
const { stat } = await api.home.saveTemp({ id: tempid, title: proxy.title || '未命名模板', content: JSON.stringify({ page: proxy.dPage, widgets: proxy.dWidgets }), width: proxy.dPage.width, height: proxy.dPage.height })
stat != 0 && useNotification('保存成功', '模板内容已变更')
const { tempid, tempType: type } = route.query
let res = null
if (type == 1) {
//
if (proxy.dWidgets[0].type === 'w-group') {
const group = proxy.dWidgets.shift()
group.record.width = 0
group.record.height = 0
proxy.dWidgets.push(group)
}
// TODO
if (!proxy.dWidgets.some((x) => x.type === 'w-group')) {
alert('提交组件必须为组合!')
return
// proxy.dWidgets.push(wGroup.setting)
}
res = await api.home.saveTemp({ id: tempid, type, title: proxy.title || '未命名组件', content: JSON.stringify(proxy.dWidgets), width: proxy.dPage.width, height: proxy.dPage.height })
} else res = await api.home.saveTemp({ id: tempid, title: proxy.title || '未命名模板', content: JSON.stringify({ page: proxy.dPage, widgets: proxy.dWidgets }), width: proxy.dPage.width, height: proxy.dPage.height })
res.stat != 0 && useNotification('保存成功', '模板内容已变更')
}
//
async function stateChange(e: any) {
const { tempid } = route.query
const { stat } = await api.home.saveTemp({ id: tempid, state: e ? 1 : 0 })
const { tempid, tempType: type } = route.query
const { stat } = await api.home.saveTemp({ id: tempid, type, state: e ? 1 : 0 })
stat != 0 && useNotification('保存成功', '模板内容已变更')
}
async function download() {
if (state.loading === true) {
return
}
//
if (proxy.title === '自设计模板') {
const isPass = await useConfirm('提示', 'PSD自设计作品暂时保存在Github下载可能失败', 'warning')
if (!isPass) {
return
}
}
state.loading = true
context.emit('update:modelValue', true)
context.emit('change', { downloadPercent: 1, downloadText: '正在处理封面' })
@ -132,8 +157,8 @@ export default defineComponent({
...mapGetters(['dPage', 'dWidgets', 'tempEditing', 'dHistory', 'dPageHistory']),
},
methods: {
...mapActions(['pushHistory']),
async load(id: any, tempId: any, cb: Function) {
...mapActions(['pushHistory', 'addGroup']),
async load(id: any, tempId: any, type: any, cb: Function) {
if (this.$route.name !== 'Draw') {
await useFontStore.init() //
}
@ -142,15 +167,22 @@ export default defineComponent({
cb()
return
}
const { data: content, title, state } = await api.home[apiName]({ id: id || tempId })
const { data: content, title, state, width, height } = await api.home[apiName]({ id: id || tempId, type })
if (content) {
const data = JSON.parse(content)
this.stateBollean = !!state
this.title = title
this.$store.commit('setShowMoveable', false) //
// this.$store.commit('setDWidgets', [])
this.$store.commit('setDPage', data.page)
id ? this.$store.commit('setDWidgets', data.widgets) : this.$store.dispatch('setTemplate', data.widgets)
if (type == 1) {
//
this.dPage.width = width
this.dPage.height = height
this.addGroup(data)
} else {
this.$store.commit('setDPage', data.page)
id ? this.$store.commit('setDWidgets', data.widgets) : this.$store.dispatch('setTemplate', data.widgets)
}
cb()
this.pushHistory('请求加载load')
}

View File

@ -12,12 +12,13 @@
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env"],
"types": ["webpack-env", "element-plus/global"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
"exclude": ["node_modules"]
}