Merge pull request #79 from palxiao/feature-vue3

Merge part vue3 & TS modified
This commit is contained in:
ShawnPhang 2024-03-10 22:05:45 +08:00 committed by GitHub
commit d65519f181
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
102 changed files with 7650 additions and 36671 deletions

View File

@ -13,6 +13,7 @@ module.exports = {
// 自定义你的规则
'vue/component-tags-order': ['off'],
'vue/no-multiple-template-root': ['off'],
'max-params': ['off'],
// 'no-undef': 'off', // 禁止使用未定义的变量会把TS声明视为变量暂时关闭
},
parserOptions: {

1
.gitignore vendored
View File

@ -15,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

@ -5,7 +5,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"css.validate": false,
"less.validate": false,

34384
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"@palxp/color-picker": "^1.5.5",
"@palxp/image-extraction": "^1.2.4",
"@scena/guides": "^0.18.1",
"@types/cropperjs": "^1.3.0",
"@webtoon/psd": "^0.4.0",
"axios": "^0.21.1",
"core-js": "^3.6.5",
@ -31,39 +32,30 @@
"qr-code-styling": "^1.6.0-rc.1",
"selecto": "^1.13.0",
"throttle-debounce": "^3.0.1",
"vue": "^3.0.0",
"vite-plugin-compression": "^0.5.1",
"vue": "3.4.19",
"vue-router": "^4.0.0-0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/node": "^16.3.1",
"@types/fontfaceobserver": "^2.1.3",
"@types/node": "^20.11.24",
"@types/throttle-debounce": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.1.4",
"@vue/eslint-config-typescript": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.3.1",
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.3",
"esbuild-loader": "^2.13.1",
"eslint": "^7.29.0",
"eslint": "^8.56.0",
"eslint-config-alloy": "~4.1.0",
"eslint-plugin-vue": "^7.12.1",
"less": "^4.1.1",
"typescript": "~4.1.5",
"terser": "^5.28.1",
"typescript": "^5.2.2",
"unplugin-element-plus": "^0.7.1",
"vite": "^2.4.1",
"vite-plugin-compression": "^0.3.0",
"vue-cli-plugin-norm": "~1.2.2",
"vue-eslint-parser": "^7.6.0",
"vue-tsc": "^0.2.0"
"vite": "^5.1.4",
"vue-tsc": "^1.8.27"
},
"browserslist": [
"> 1%",

View File

@ -2,23 +2,32 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 14:42:15
* @Description: AI相关接口
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-13 00:07:19
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-03 19:00:00
*/
import fetch from '@/utils/axios'
export type TCommonUploadCb = (up: number, dp: number) => void
type TUploadProgressCbData = {
loaded: number
total: number
}
export type TUploadErrorResult = {type: "application/json"}
// 上传接口
export const upload = (file: File, cb: Function) => {
export const upload = (file: File, cb: TCommonUploadCb) => {
const formData = new FormData()
formData.append('file', file)
const extra = {
responseType: 'blob',
onUploadProgress: (progress: any) => {
onUploadProgress: (progress: TUploadProgressCbData) => {
cb(Math.floor((progress.loaded / progress.total) * 100), 0)
},
onDownloadProgress: (progress: any) => {
onDownloadProgress: (progress: TUploadProgressCbData) => {
cb(100, Math.floor((progress.loaded / progress.total) * 100))
},
}
return fetch('https://res.palxp.cn/ai/upload', formData, 'post', {}, extra)
return fetch<MediaSource | TUploadErrorResult>('https://res.palxp.cn/ai/upload', formData, 'post', {}, extra)
}

View File

@ -21,7 +21,12 @@ export const init = (params: Type.Object = {}) => fetch(API.init, params, 'post'
export const getPicList = (params: Type.Object = {}) => fetch(API.getList, params)
export const getToken = (params: Type.Object = {}) => fetch(API.getToken, params)
type TGetTokenParam = {
bucket: string,
name: string
}
export const getToken = (params: TGetTokenParam) => fetch<string>(API.getToken, params)
export const deletePic = (params: Type.Object = {}) => fetch(API.delOne, params, 'post')

View File

@ -11,10 +11,57 @@ import _config from '@/config'
// const screenshot_url = window.location.protocol + '//' + window.location.host + '/draw'
export const download = (params: Type.Object = {}) => `${_config.SCREEN_URL}/api/screenshots?id=${params.id}&width=${params.width}&height=${params.height}`
type IGetTempListParam = {
search: string
page: number
pageSize: number
cate: number | string
}
export type IGetTempListData = {
cover: string
height: number
id: number
state: number
title: string
width: number
isDelect: boolean
fail: boolean
top: number
left: number
data?: string
listWidth?: number
gap?: number
thumb?: string
url: string
model?: string
color?: string
}
type IGetTempListResult = TPageRequestResult<IGetTempListData[]>
// 获取模板列表
export const getTempList = (params: Type.Object = {}) => fetch('design/list', params, 'get')
export const getTempDetail = (params: Type.Object = {}) => fetch('design/temp', params, 'get')
export const getCategories = (params: Type.Object = {}) => fetch('design/cate', params, 'get')
export const getTempList = (params: IGetTempListParam) => fetch<IGetTempListResult>('design/list', params, 'get')
type TGetTempDetail = {
id: number
type?: number
}
export const getTempDetail = (params: TGetTempDetail) => fetch<{data: string}>('design/temp', params, 'get')
type TGetCategoriesParams = {
type?: number
}
export type TGetCategoriesData = {
id: number
name: string
pid: number
type: number
}
type TgetCategoriesResult = TCommResResult<TGetCategoriesData>
export const getCategories = (params: TGetCategoriesParams) => fetch<TgetCategoriesResult[]>('design/cate', params, 'get')
// 保存模板
export const saveTemp = (params: Type.Object = {}) => fetch('design/edit', params, 'post')
// export const delTemp = (params: Type.Object = {}) => fetch('/api/template/temp_del', params)
@ -33,5 +80,12 @@ export const saveMyTemp = (params: Type.Object = {}) => fetch('design/user/temp'
// 获取作品
export const getWorks = (params: Type.Object = {}) => fetch('design/poster', params, 'get')
type TGetMyDesignParams = {
page: number
pageSize: number
}
type TGetMyDesignResult = TPageRequestResult<IGetTempListData[]>
// 作品列表
export const getMyDesign = (params: Type.Object = {}) => fetch('design/my', params, 'get')
export const getMyDesign = (params: TGetMyDesignParams) => fetch<TGetMyDesignResult>('design/my', params, 'get')

View File

@ -6,22 +6,82 @@
* @LastEditTime: 2023-12-11 11:40:47
*/
import fetch from '@/utils/axios'
import { IGetTempListData } from './home'
// 获取素材分类:
export const getKinds = (params: Type.Object = {}) => fetch('design/cate', params)
type TGetListParam = {
first_id?: number
second_id?: string
cate?: number
pageSize?: number
}
export type TGetListData = {
category: number
created_time: string
height: number
id: number
model: string
original: string
state: number
thumb: string
title: string
type: string
updated_time: string
url: string
width: number
thumbUrl: string
imgUrl: string
}
export type TGetListResult = TPageRequestResult<TGetListData[]>
// 获取素材列表:
export const getList = (params: Type.Object = {}) => fetch('design/material', params)
export const getList = (params: TGetListParam) => fetch<TGetListResult>('design/material', params)
export type TGetFontParam = {
pageSize?: number
}
/** 字体item数据 */
export type TGetFontItemData = {
id: number
alias: string
oid: string
value: string
preview: string
woff: string
lang: string
}
// 获取字体
export const getFonts = (params: Type.Object = {}) => fetch('design/fonts', params)
export const getFonts = (params: TGetFontParam = {}) => fetch<TPageRequestResult<TGetFontItemData[]>>('design/fonts', params)
export const getFontSub = (params: Type.Object = {}, extra: any = {}) => fetch('design/font_sub', params, 'get', {}, extra)
type TGetImageListParams = {
page?: number
cate?: number
}
export type TGetImageListResult = {
created_time: string
height: number
width: number
url: string
user_id: number
id: string
thumb: string
} & IGetTempListData
// 图库列表
export const getImagesList = (params: Type.Object = {}) => fetch('design/imgs', params, 'get')
export const getImagesList = (params: TGetImageListParams) => fetch<TPageRequestResult<TGetImageListResult[]>>('design/imgs', params, 'get')
// 我的上传列表
export const getMyPhoto = (params: Type.Object = {}) => fetch('design/user/image', params)
export const getMyPhoto = (params: TGetImageListParams) => fetch<TPageRequestResult<TGetImageListResult[]>>('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')

View File

@ -2,9 +2,17 @@
* @Author: ShawnPhang
* @Date: 2022-02-12 11:08:57
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:35:44
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
export type AlignListData = {
key: string
icon: string
tip: string
value: string
}
export default [
{
key: 'align',
@ -42,4 +50,4 @@ export default [
tip: '下对齐',
value: 'bottom',
},
]
] as AlignListData[]

View File

@ -2,9 +2,17 @@
* @Author: ShawnPhang
* @Date: 2022-04-15 10:51:50
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-04-15 10:51:51
* @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
export type LayerIconList = {
key: string
icon: string
tip: string
value: number
}
export default [
{
key: 'zIndex',
@ -18,4 +26,4 @@ export default [
tip: '下一层',
value: -1,
},
]
] as LayerIconList[]

View File

@ -2,9 +2,21 @@
* @Author: ShawnPhang
* @Date: 2022-03-16 11:38:48
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-03-23 16:00:11
* @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
export type QrCodeLocalizationData = {
dotColorTypes: {
key: string
value: string
}[]
dotTypes: {
key: string
value: string
}[]
}
export default {
dotColorTypes: [
{
@ -42,4 +54,4 @@ export default {
value: '特殊风格',
},
],
}
} as QrCodeLocalizationData

View File

@ -2,10 +2,20 @@
* @Author: ShawnPhang
* @Date: 2021-08-02 18:27:27
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-25 10:30:38
* @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
import { AlignListData } from "./AlignListData"
export type TStyleIconData = {
key: string
icon: string
tip: string
value: string[]
select: boolean
}
export const styleIconList1 = [
{
key: 'fontWeight',
@ -42,7 +52,16 @@ export const styleIconList1 = [
value: ['horizontal-tb', 'vertical-rl'], // tb-rl
select: false,
},
]
] as TStyleIconData[]
export type TStyleIconData2 = {
key: string
icon: string
tip: string
value: string
select: boolean
}
export const styleIconList2 = [
{
key: 'textAlign',
@ -72,7 +91,7 @@ export const styleIconList2 = [
value: 'justify',
select: false,
},
]
] as TStyleIconData2[]
export const alignIconList = [
{
@ -111,4 +130,4 @@ export const alignIconList = [
tip: '下对齐',
value: 'bottom',
},
]
] as AlignListData[]

View File

@ -2,9 +2,20 @@
* @Author: ShawnPhang
* @Date: 2021-07-17 11:20:22
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-24 17:07:44
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
import { StyleValue } from "vue"
export type TWidgetClassifyData = {
name: string
icon: string
show: boolean
component: string
style?: StyleValue
}
export default [
{
name: '模板',
@ -49,4 +60,4 @@ export default [
show: false,
component: 'user-wrap',
},
]
] as TWidgetClassifyData[]

View File

@ -5,17 +5,34 @@
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-22 18:11:15
*/
import store from '@/store'
export default class dragHelper {
private cloneEl: any = null
type TInitial = {
offsetX: number
offsetY: number
pageX: number
pageY: number
width: number
height: number
finallySize: number
flag: number
x: number
y: number
}
type TQueueFunction = () => void
export default class DragHelper {
private cloneEl?: HTMLElement | null
private dragging: boolean = false
private initial: any = {}
private queue: any = []
private initial: Partial<TInitial> = {}
private queue: TQueueFunction[] = []
constructor() {
window.addEventListener('mousemove', (e) => {
if (this.dragging && this.cloneEl) {
const { offsetX, offsetY, width, height } = this.initial
const { width, height } = this.initial as TInitial
// 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 {
@ -23,10 +40,18 @@ export default class dragHelper {
}
})
// 鼠标抬起
window.addEventListener('mouseup', (e: any) => {
;(window as any).document.getElementById('app').classList.remove('drag_active')
const cl = e.target.classList
if (e.target?.id === 'page-design-canvas' || cl.contains('target') || cl.contains('drop__mask') || cl.contains('edit-text')) {
window.addEventListener('mouseup', (e) => {
const el = window.document.getElementById('app')
if (!el || !e.target) return
el.classList.remove('drag_active')
const target = e.target as HTMLElement
const cl = target.classList
if (
target.id === 'page-design-canvas' ||
cl.contains('target') ||
cl.contains('drop__mask') ||
cl.contains('edit-text')
) {
setTimeout(() => {
this.finish(true)
}, 10)
@ -44,29 +69,39 @@ export default class dragHelper {
/**
* mousedown
*/
public start(e: any, finallySize: any) {
public start(e: MouseEvent, finallySize: number) {
if (!this.cloneEl) {
store.commit('setDraging', true)
;(window as any).document.getElementById('app').classList.add('drag_active') // 整个鼠标全局变成抓取
const app = window.document.getElementById('app')
if (!app || !e) return
app.classList.add('drag_active') // 整个鼠标全局变成抓取
const target = e.target as HTMLElement
// 选中了元素
this.cloneEl = e.target.cloneNode(true)
this.cloneEl = (target.cloneNode(true) as HTMLElement)
this.cloneEl.classList.add('flutter')
// 初始化数据
this.init(e, e.target, finallySize || e.target.offsetWidth, Math.random())
this.init(e, target, finallySize || target.offsetWidth, Math.random())
// 加载原图
// simulate(cloneEl.src, initial.flag)
this.cloneEl.style.width = e.target.offsetWidth
this.cloneEl.style.width = `${target.offsetWidth}`
// e.target.parentElement.parentElement.appendChild(this.cloneEl)
;(window as any).document.getElementById('widget-panel').appendChild(this.cloneEl)
const widgetPanel = window.document.getElementById('widget-panel')
if (!widgetPanel) return
widgetPanel.appendChild(this.cloneEl)
this.dragging = true
e.target.classList.add('hide') // 放在最后
target.classList.add('hide') // 放在最后
this.queue.push(() => {
e.target.classList.remove('hide')
target.classList.remove('hide')
})
}
}
// 开始拖动初始化
private init({ offsetX, offsetY, pageX, pageY, x, y }: any, { offsetWidth: width, offsetHeight: height }: any, finallySize: number, flag: any) {
private init(
{ offsetX, offsetY, pageX, pageY, x, y }: MouseEvent,
{ offsetWidth: width, offsetHeight: height }: HTMLElement,
finallySize: number,
flag: number
) {
this.initial = { offsetX, offsetY, pageX, pageY, width, height, finallySize, flag, x, y }
// store.commit('setDragInitData', { offsetX: 0, offsetY: 0 })
this.moveFlutter(pageX - offsetX, pageY - offsetY, 0, 0.3)
@ -76,17 +111,23 @@ export default class dragHelper {
}
// 改变漂浮元素(合并多个操作)
private moveFlutter(x: number, y: number, d = 0, lazy = 0) {
const { width, height, finallySize } = this.initial
let scale: any = null
if (width > finallySize) {
scale = d ? (width - d >= finallySize ? `transform: scale(${(width - d) / width});` : null) : null
} else scale = d ? (width + d <= finallySize ? `transform: scale(${(width + d) / width})` : null) : null
const { width, height, finallySize } = this.initial as TInitial
let scale: string | null = null
if (d) {
if (width > finallySize) {
scale = width - d >= finallySize ? `transform: scale(${(width - d) / width});` : null
} else {
scale = width + d <= finallySize ? `transform: scale(${(width + d) / width})` : null
}
}
const options = [`left: ${x}px`, `top: ${y}px`, `width: ${width}px`, `height: ${height}px`]
scale && options.push(scale)
options.push(`transition: all ${lazy}s`)
this.changeStyle(options)
}
private changeStyle(arr: any) {
private changeStyle(arr: string[]) {
if (!this.cloneEl) return
const original = this.cloneEl.style.cssText.split(';')
original.pop()
this.cloneEl.style.cssText = original.concat(arr).join(';') + ';'
@ -99,16 +140,14 @@ export default class dragHelper {
this.dragging = false
store.commit('setDraging', false)
store.commit('selectItem', {})
if (!this.cloneEl) {
return
}
if (!done) {
const { pageX, offsetX, pageY, offsetY } = this.initial
const { pageX, offsetX, pageY, offsetY } = this.initial as TInitial
this.changeStyle([`left: ${pageX - offsetX}px`, `top: ${pageY - offsetY}px`, 'transform: scale(1)', 'transition: all 0.3s'])
}
setTimeout(
() => {
this.queue.length && this.queue.shift()()
this.queue.length && (this.queue.shift() as TQueueFunction)()
this.cloneEl && this.cloneEl.remove()
this.cloneEl = null
},
@ -116,8 +155,8 @@ export default class dragHelper {
)
}
// 计算两点之间距离
private distance({ pageX, pageY }: any) {
const { pageX: x, pageY: y } = this.initial
private distance({ pageX, pageY }: { pageX: number, pageY: number }) {
const { pageX: x, pageY: y } = this.initial as TInitial
return Math.hypot(pageX - x, pageY - y)
}
}

View File

@ -0,0 +1,13 @@
import { ComputedRef, computed } from 'vue'
import { useStore } from 'vuex'
export function useSetupMapGetters<T extends string>(strList: T[]) {
const mapData: Partial<{[x in T]: ComputedRef}> = {}
const getters = useStore().getters
strList.forEach(val => {
mapData[val] = computed(() => getters[val])
})
return mapData as {[x in T]: ComputedRef}
}

View File

@ -2,19 +2,19 @@
* @Author: ShawnPhang
* @Date: 2022-02-22 15:06:14
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-03-07 14:57:51
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
import store from '@/store'
export default async function setCompData(item: any) {
const group = typeof item === 'string' ? JSON.parse(item) : JSON.parse(JSON.stringify(item))
let parent: any = {}
export default async function setCompData(item: TCommonItemData[] | string) {
const group: TCommonItemData[] = typeof item === 'string' ? JSON.parse(item) : JSON.parse(JSON.stringify(item))
let parent: Partial<TCommonItemData> = {}
Array.isArray(group) &&
group.forEach((element: any) => {
group.forEach((element) => {
element.type === 'w-group' && (parent = element)
})
const { width: screenWidth, height: screenHeight } = store.getters.dPage
const { width: imgWidth, height: imgHeight } = parent
const { width: imgWidth = 0, height: imgHeight = 0 } = parent
let ratio = 1
// 先限制在画布内,保证不超过边界
if (imgWidth > screenWidth || imgHeight > screenHeight) {
@ -23,7 +23,7 @@ export default async function setCompData(item: any) {
// 根据画布缩放比例再进行一次调整
if (ratio < 1) {
ratio *= store.getters.dZoom / 100
group.forEach((element: any) => {
group.forEach((element) => {
element.fontSize && (element.fontSize *= ratio)
element.width *= ratio
element.height *= ratio

View File

@ -2,17 +2,33 @@
* @Author: ShawnPhang
* @Date: 2022-02-22 15:06:14
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-11 17:36:44
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-01 20:55:51
*/
import store from '@/store'
import { getImage } from '../getImgDetail'
export default async function setItem2Data(item: any) {
export type TItem2DataParam = {
id?: string | number
width: number
height: number
url: string
model?: string
canvasWidth?: number
}
export type TItem2DataResult = {
width: number
height: number
canvasWidth: number
}
export default async function setItem2Data(item: TItem2DataParam): Promise<Required<TItem2DataParam>> {
const cloneItem = JSON.parse(JSON.stringify(item))
const { width: screenWidth, height: screenHeight } = store.getters.dPage
let { width: imgWidth, height: imgHeight } = item
if (!imgWidth || !imgHeight) {
const actual: any = await getImage(item.url)
const actual = await getImage(item.url)
cloneItem.width = imgWidth = actual.width
cloneItem.height = imgHeight = actual.height
}

View File

@ -11,7 +11,8 @@ import setImageData from '@/common/methods/DesignFeatures/setImage'
import wText from '@/components/modules/widgets/wText/wText.vue'
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
import wSvg from '@/components/modules/widgets/wSvg/wSvg.vue'
export default async function(type: string, item: any, data: any) {
export default async function(type: string, item: TCommonItemData, data: Record<string, any>) {
let setting = data
if (type === 'text') {
!item.fontFamily && !item.color ? (setting = JSON.parse(JSON.stringify(wText.setting))) : (setting = item)

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-29 20:35:31
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 16:11:55
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
import dayjs from 'dayjs'
import api from '@/api/album'
@ -15,13 +15,13 @@ interface Options {
}
export default {
upload: async (file: File, options: Options, cb?: Function) => {
const win: any = window
upload: async (file: File | Blob, options: Options, cb?: IQiniuSubscribeCb) => {
const win = window
let name = ''
const suffix = file.type.split('/')[1] || 'png' // 文件后缀
if (!options.fullPath) {
// const DT: any = await exifGetTime(file) // 照片时间
const DT: any = new Date()
const DT = new Date()
const YM = `${dayjs(DT).format('YYYY')}/${dayjs(DT).format('MM')}/` // 文件时间分类
const keyName = YM + new Date(DT).getTime()
const prePath = options.prePath ? options.prePath + '/' : ''
@ -32,15 +32,15 @@ export default {
useCdnDomain: true, // 使用cdn加速
}
const observable = win.qiniu.upload(file, name, token, {}, exOption)
return new Promise((resolve: Function, reject: Function) => {
return new Promise((resolve: IQiniuSubscribeCb, reject: (err: string) => void) => {
observable.subscribe({
next: (result: any) => {
cb && cb(result) // result.total.percent -> 展示进度
next: (result) => {
cb?.(result) // result.total.percent -> 展示进度
},
error: (e: any) => {
error: (e) => {
reject(e)
},
complete: (result: any) => {
complete: (result) => {
resolve(result)
// cb && cb(result) // result.total.percent -> 展示进度
},

View File

@ -2,12 +2,19 @@
* @Author: ShawnPhang
* @Date: 2022-03-25 13:43:07
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-03-25 14:32:19
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
import store from '@/store'
export default function(el: Element | string, cb: Function, altLimit: boolean = true) {
type TAddEventCb = (e: Event) => void
type TAddEventObj = {
attachEvent?: HTMLElement["addEventListener"]
} & HTMLElement
export default function(el: HTMLElement | string, cb: Function, altLimit: boolean = true) {
const box = typeof el === 'string' ? document.getElementById(el) : el
if (!box) return;
addEvent(box, 'mousewheel', (e: any) => {
const ev = e || window.event
const down = ev.wheelDelta ? ev.wheelDelta < 0 : ev.detail > 0
@ -16,10 +23,7 @@ export default function(el: Element | string, cb: Function, altLimit: boolean =
// } else {
// console.log('鼠标滚轮向上++++++++++')
// }
if (altLimit && store.getters.dAltDown) {
ev.preventDefault()
cb(down)
} else if (!altLimit) {
if ((altLimit && store.getters.dAltDown) || !altLimit) {
ev.preventDefault()
cb(down)
}
@ -27,7 +31,7 @@ export default function(el: Element | string, cb: Function, altLimit: boolean =
})
}
function addEvent(obj: any, xEvent: string, fn: Function) {
function addEvent(obj: TAddEventObj, xEvent: keyof HTMLElementEventMap, fn: TAddEventCb) {
if (obj.attachEvent) {
obj.attachEvent('on' + xEvent, fn)
} else {

View File

@ -2,11 +2,11 @@
* @Author: ShawnPhang
* @Date: 2022-02-03 16:30:18
* @Description: Type: success / info / warning / error
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-03 16:43:01
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
import { ElMessageBox } from 'element-plus'
export default (title: string = '提示', message: string = '', type: any = 'success') => {
import { ElMessageBox, messageType } from 'element-plus'
export default (title: string = '提示', message: string = '', type: messageType = 'success') => {
return new Promise((resolve: Function) => {
ElMessageBox.confirm(message, title, {
confirmButtonText: '确定',

View File

@ -2,11 +2,14 @@
* @Author: ShawnPhang
* @Date: 2021-09-30 15:52:59
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-12 16:54:51
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
export default (src: string, cb: Function) => {
return new Promise((resolve: any) => {
type TCallBack = (progress: number, xhr: XMLHttpRequest) => void
export default (src: string, cb: TCallBack) => {
return new Promise<void>((resolve) => {
// const image = new Image()
// // 解决跨域 Canvas 污染问题
// image.setAttribute('crossOrigin', 'anonymous')
@ -32,10 +35,10 @@ export default (src: string, cb: Function) => {
fetchImageDataFromUrl(src, (progress: number, xhr: XMLHttpRequest) => {
cb(progress, xhr)
}).then((res: any) => {
}).then((res) => {
const reader = new FileReader()
reader.onload = function (event) {
const txt: any = event?.target?.result
const txt = event?.target?.result as string
// image.src = txt
const a = document.createElement('a')
const mE = new MouseEvent('click')
@ -55,17 +58,17 @@ export default (src: string, cb: Function) => {
})
}
function fetchImageDataFromUrl(url: string, cb: Function) {
return new Promise((resolve) => {
function fetchImageDataFromUrl(url: string, cb: TCallBack) {
return new Promise<null>((resolve) => {
const xhr = new XMLHttpRequest()
let totalLength: any = ''
let totalLength: string | number = ''
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onreadystatechange = function () {
totalLength = Number(xhr.getResponseHeader('content-length')) // 'cache-control'
}
xhr.onprogress = function (event) {
cb((event.loaded / totalLength) * 100, xhr)
cb((event.loaded / Number(totalLength)) * 100, xhr)
}
xhr.onload = function () {
if (xhr.status < 400) resolve(this.response)

View File

@ -6,11 +6,14 @@
* @LastEditTime: 2023-10-13 01:30:33
*/
// import { isSupportFontFamily, blob2Base64 } from './utils'
import { getFonts } from '@/api/material'
import { TGetFontItemData, getFonts } from '@/api/material'
const nowVersion = '2' // 当前字体文件版本更新,将刷新前端缓存
const fontList: any = []
/** 字体item类型 */
export type TFontItemData = { url: string } & Omit<TGetFontItemData, "woff">
const fontList: TFontItemData[] = []
// const download: any = {}
export const useFontStore = {
list: fontList,
@ -18,7 +21,7 @@ export const useFontStore = {
async init() {
this.list = []
localStorage.getItem('FONTS_VERSION') !== nowVersion && localStorage.removeItem('FONTS')
const localFonts: any = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
const localFonts: TFontItemData[] = localStorage.getItem('FONTS') ? JSON.parse(localStorage.getItem('FONTS') || '') : []
if (localFonts.length > 0) {
this.list.push(...localFonts)
}
@ -26,7 +29,7 @@ export const useFontStore = {
if (this.list.length === 0) {
const res = await getFonts({ pageSize: 400 })
this.list.unshift(
...res.list.map((x: any) => {
...res.list.map((x) => {
const { id, alias, oid, value, preview, woff, lang } = x
return { id, oid, value, preview, alias, url: woff, lang }
}),

View File

@ -70,27 +70,27 @@ export function generateFontStyle(name: string, url: string): HTMLStyleElement {
}
// 找到使用到的所有字体
export function filterSkyFonts() {
const fonts: string[] = []
// const textClouds = sky.state.clouds.filter(
// (cloud) => cloud.type === CLOUD_TYPE.text,
// );
const textClouds: any = []
// export function filterSkyFonts() {
// const fonts: string[] = []
// // const textClouds = sky.state.clouds.filter(
// // (cloud) => cloud.type === CLOUD_TYPE.text,
// // );
// const textClouds: any = []
;(textClouds as unknown as CloudText[]).forEach((cloud) => {
// 找到文字组件字体
if (cloud.fontFamily && !fonts.includes(cloud.fontFamily)) {
fonts.push(cloud.fontFamily)
}
// 找到文字组件子级字体
cloud.texts.forEach((text) => {
if (text.fontFamily && !fonts.includes(text.fontFamily)) {
fonts.push(text.fontFamily)
}
})
})
return fonts
}
// ;(textClouds as unknown as CloudText[]).forEach((cloud) => {
// // 找到文字组件字体
// if (cloud.fontFamily && !fonts.includes(cloud.fontFamily)) {
// fonts.push(cloud.fontFamily)
// }
// // 找到文字组件子级字体
// cloud.texts.forEach((text) => {
// if (text.fontFamily && !fonts.includes(text.fontFamily)) {
// fonts.push(text.fontFamily)
// }
// })
// })
// return fonts
// }
export function base642Blob(b64Data: string, contentType = '', sliceSize = 512) {
const byteCharacters = atob(b64Data)

View File

@ -5,7 +5,7 @@
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 10:42:54
*/
export const getImage = (imgItem: string | File) => {
export const getImage = (imgItem: string | File): Promise<HTMLImageElement> => {
// 创建对象
const img = new Image()

View File

@ -2,17 +2,17 @@
* @Author: ShawnPhang
* @Date: 2022-01-31 10:45:53
* @Description: transform字符串
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-02-18 16:54:13
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 11:50:00
*/
export function getTransformAttribute(target: any, attr: string = '') {
export function getTransformAttribute(target: HTMLElement, attr: string = '') {
const tf = target.style.transform
const iof = tf.indexOf(attr)
const half = tf.substring(iof + attr.length + 1)
return half.substring(0, half.indexOf(')'))
}
export function setTransformAttribute(target: any, attr: string, value: string | number = 0) {
export function setTransformAttribute(target: HTMLElement, attr: string, value: string | number = 0) {
const tf = target?.style.transform
if (!tf) {
return
@ -24,10 +24,10 @@ export function setTransformAttribute(target: any, attr: string, value: string |
target.style.transform = FRONT + value + END
}
export function getMatrix(params: any) {
export function getMatrix(params: Record<string, any>) {
const result = []
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
if (Object.hasOwn(params, key)) {
result.push(params[key])
}
}

View File

@ -8,17 +8,17 @@
// TODO: Group类型比较特殊所以需要全量循环并判断是否为group
const arr = ['w-text', 'w-image', 'w-svg', 'w-group', 'w-qrcode']
export function getTarget(currentTarget: any) {
let collector: any[] = []
let groupTarger: any = null
let saveTarger: any = null
export function getTarget(currentTarget: HTMLElement): Promise<HTMLElement | null> {
let collector: string[] = []
let groupTarger: HTMLElement | null = null
let saveTarger: HTMLElement | null = null
return new Promise((resolve) => {
function findTarget(target: any) {
function findTarget(target: HTMLElement | null) {
if (!target || target.id === 'page-design') {
if (collector.length > 1) {
resolve(groupTarger)
} else {
resolve(saveTarger || currentTarget)
resolve(saveTarger ?? currentTarget)
}
return
}
@ -37,12 +37,12 @@ export function getTarget(currentTarget: any) {
})
}
export function getFinalTarget(currentTarget: any) {
let collector: any[] = []
let groupTarger: any = null
let saveTarger: any = null
export function getFinalTarget(currentTarget: HTMLElement) {
let collector: string[] = []
// let groupTarger: HTMLElement | null = null
// let saveTarger: HTMLElement | null = null
return new Promise((resolve) => {
function findTarget(target: any) {
function findTarget(target: HTMLElement | null) {
if (!target || target.id === 'page-design') {
resolve(target)
return
@ -51,8 +51,8 @@ export function getFinalTarget(currentTarget: any) {
collector = collector.concat(
t.filter((x) => {
arr.includes(x) && (saveTarger = target)
x === 'w-group' && (groupTarger = target)
// arr.includes(x) && (saveTarger = target)
// x === 'w-group' && (groupTarger = target)
return arr.includes(x)
}),
)

View File

@ -8,7 +8,7 @@
<template>
<el-dialog v-model="dialogVisible" title="裁剪图片" width="80%" :before-close="handleClose" @close="cancel">
<div id="wrap" v-loading="loading" style="height: 50vh">
<img v-show="url" ref="imgBox" style="visibility: hidden" :src="url" />
<img v-show="url" ref="imgBox" style="visibility: hidden" alt="imgBox" :src="url" />
</div>
<template #footer>
<span class="dialog-footer">
@ -84,6 +84,7 @@ export default defineComponent({
cancel()
}, 100)
}
const cancel = () => {
store.commit('setShowMoveable', true)
dialogVisible.value = false

View File

@ -0,0 +1,114 @@
<!--
* @Author: ShawnPhang
* @Date: 2024-03-02 13:32:00
* @Description: 裁剪组件
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-02 13:32:00
-->
<template>
<el-dialog v-model="dialogVisible" title="裁剪图片" width="80%" :before-close="handleClose" @close="cancel">
<div id="wrap" v-loading="state.loading" style="height: 50vh">
<img v-show="state.url" ref="imgBox" style="visibility: hidden" alt="imgBox" :src="state.url" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel">取消</el-button>
<el-button :loading="state.loading" plain type="primary" @click="ok">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '@/api'
import { ElDialog } from 'element-plus'
import { ref, defineEmits, reactive, nextTick, toRefs } from 'vue'
import { useStore } from 'vuex'
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
type TDoneParams = {
newImg: string,
data: Cropper.Data,
width: string,
height: string
}
type TDoneFunc = (event: "done", data: TDoneParams) => void
type TOpenItem = {
rawImg?: string
imgUrl: string
}
const store = useStore()
const state = reactive({
loading: false,
url: '',
})
const dialogVisible = ref(false)
const imgBox = ref<HTMLImageElement>()
let cropData: Cropper.Data | null
let cropper: Cropper
const emit = defineEmits<TDoneFunc>()
const handleClose = (done: () => void) => {
done()
}
const open = async (item: TOpenItem, data: Cropper.Data) => {
state.loading = true
item.rawImg = item.rawImg ? item.rawImg : item.imgUrl
cropData = data
state.url = item.rawImg
store.commit('setShowMoveable', false)
dialogVisible.value = true
await nextTick()
setEdit()
}
const setEdit = () => {
if (!imgBox || !imgBox.value) return
cropper = new Cropper(imgBox.value, {
// aspectRatio: imgBox.value.width / imgBox.value.height,
dragMode: 'move',
viewMode: 1,
cropBoxMovable: false,
// cropBoxResizable: false,
highlight: false,
background: true,
// crop(event) {
// console.log(event);
// },
})
imgBox.value.addEventListener('ready', function() {
state.loading = false
// if (this.cropper === cropper) {
cropData && cropper.setData(cropData)
// }
})
}
const ok = () => {
state.loading = true
setTimeout(async () => {
// const newImg = cropper.getCroppedCanvas({ maxWidth: 4096, minWidth: 4096 }).toDataURL('image/jpeg', 0.8)
// const { width, height } = cropper.getCropBoxData()
// const { preview_url } = await api.material.uploadBase64({ file: newImg })
// context.emit('done', { newImg: preview_url, data: cropper.getData(), width, height })
cancel()
}, 100)
}
const cancel = () => {
store.commit('setShowMoveable', true)
dialogVisible.value = false
state.url = ''
cropData = null
state.loading = false
cropper.destroy()
}
toRefs(state),
</script>

View File

@ -1,222 +0,0 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-07-11 23:50:22
* @Description: 抠图组件
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-09 00:42:48
-->
<template>
<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">服务器带宽过低为了更好的体验请上传 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 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>
</div>
<template #footer>
<span class="dialog-footer">
<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, 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, ElProgress, ImageExtraction },
emits: ['done'],
setup(props, { emit }) {
const store = useStore()
const state: any = reactive({
show: false,
rawImage: '',
cutImage: '',
raw: null,
offsetWidth: 0,
percent: 0,
progress: 0,
progressText: '',
toolModel: true,
loading: false,
matting: null,
})
let fileName: string = 'unknow'
let isRuning: boolean = false
const selectFile = async (file: File) => {
if (file.size > 1024 * 1024 * 2) {
alert('上传图片超出限制')
return false
}
//
state.raw.addEventListener('load', () => {
state.offsetWidth = state.raw.offsetWidth
})
state.rawImage = URL.createObjectURL(file)
fileName = file.name
//
const result: 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 = (file: File) => {
state.loading = false
state.show = true
store.commit('setShowMoveable', false)
nextTick(() => {
if (file) {
selectFile(file)
state.toolModel = false
}
})
}
const handleClose = () => {
store.commit('setShowMoveable', true)
}
const mousemove = (e: MouseEvent) => {
!isRuning && (state.percent = (e.offsetX / (e.target as any).width) * 100)
}
const download = () => {
_dl.downloadBase64File(state.cutImage, fileName)
}
const clear = () => {
URL.revokeObjectURL(state.rawImage)
state.rawImage = ''
// URL.revokeObjectURL(state.cutImage)
state.cutImage = ''
state.percent = 0
state.offsetWidth = 0
}
const run = () => {
state.percent += 1
isRuning = true
state.percent < 100 ? requestAnimationFrame(run) : (isRuning = false)
}
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,
mousemove,
selectFile,
open,
handleClose,
...toRefs(state),
cutDone,
edit,
}
},
})
</script>
<style lang="less" scoped>
.uploader {
&__box {
color: #333333;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
}
.content {
position: relative;
display: flex;
justify-content: center;
}
.scan-effect {
position: relative;
height: 50vh;
overflow: hidden;
img {
// width: 100%;
height: 100%;
object-fit: contain;
position: absolute;
}
}
.scan-line {
position: absolute;
top: 0;
width: 1.5px;
height: 100%;
background: rgba(255, 255, 255, 0.7);
// background-image: linear-gradient(to top, transparent, rgba(255, 255, 255, 0.7), transparent);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
.progress {
width: 100%;
}
</style>

View File

@ -0,0 +1,201 @@
<!--
* @Author: ShawnPhang
* @Date: 2024-03-03 19:00:00
* @Description: 裁剪组件
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-03 19:00:00
-->
<template>
<el-dialog v-model="state.show" title="AI 智能抠图" align-center width="650" @close="handleClose">
<uploader v-if="!state.rawImage" :hold="true" :drag="true" :multiple="true" class="uploader" @load="handleUploaderLoad">
<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">服务器带宽过低为了更好的体验请上传 2M 内的图片</div>
</uploader>
<el-progress v-if="!state.cutImage && state.progressText" :percentage="state.progress">
<el-button text>
{{ state.progressText }} <span v-show="state.progress">{{ state.progress }}%</span>
</el-button>
</el-progress>
<div class="content">
<div v-show="state.rawImage" v-loading="!state.cutImage" :style="{ width: state.offsetWidth ? state.offsetWidth + 'px' : '100%' }" class="scan-effect transparent-bg">
<img ref="raw" :style="{ 'clip-path': 'inset(0 0 0 ' + state.percent + '%)' }" :src="state.rawImage" alt="" />
<img v-show="state.cutImage" :src="state.cutImage" alt="结果图像" @mousemove="mousemove" />
<div v-show="state.cutImage" :style="{ left: state.percent + '%' }" class="scan-line"></div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button v-show="state.rawImage && state.toolModel" @click="clear">清空重选</el-button>
<el-button v-show="state.cutImage" type="primary" plain @click="edit">进入编辑模式</el-button>
<el-button v-show="state.cutImage && state.toolModel" type="primary" plain @click="download"> 下载 </el-button>
<el-button v-show="state.cutImage && !state.toolModel" v-loading="state.loading" type="primary" plain @click="cutDone"> {{ state.loading ? '上传中..' : '完成抠图' }} </el-button>
</span>
</template>
<ImageExtraction ref="matting" />
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, nextTick, defineEmits, ref } 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 ImageExtraction from '../ImageExtraction/index.vue'
import { selectImageFile, uploadCutPhotoToCloud } from './method'
export type TImageCutoutState = {
show: boolean;
rawImage: string;
cutImage: string;
offsetWidth: number;
percent: number;
progress: number;
progressText: string;
toolModel: boolean;
loading: boolean;
}
const store = useStore()
const state = reactive<TImageCutoutState>({
show: false,
rawImage: '',
cutImage: '',
offsetWidth: 0,
percent: 0,
progress: 0,
progressText: '',
toolModel: true,
loading: false,
})
let fileName: string = 'unknow'
let isRuning: boolean = false
const emits = defineEmits<{
(event: "done", data: string): void
}>()
const raw = ref(null)
const matting = ref<typeof ImageExtraction | null>(null)
const open = (file: File) => {
state.loading = false
state.show = true
store.commit('setShowMoveable', false)
nextTick(() => {
if (file) {
handleUploaderLoad(file)
}
})
}
defineExpose({
open
})
const handleUploaderLoad = (file: File) => {
selectImageFile(state as TImageCutoutState, raw, file, (result, name) => {
fileName = name
const resultImage = URL.createObjectURL(result)
state.rawImage && (state.cutImage = resultImage)
requestAnimationFrame(run)
})
state.toolModel = false
}
const handleClose = () => {
store.commit('setShowMoveable', true)
}
const mousemove = (e: MouseEvent) => {
!isRuning && (state.percent = (e.offsetX / (e.target as any).width) * 100)
}
const download = () => {
_dl.downloadBase64File(state.cutImage, fileName)
}
const clear = () => {
URL.revokeObjectURL(state.rawImage)
state.rawImage = ''
// URL.revokeObjectURL(state.cutImage)
state.cutImage = ''
state.percent = 0
state.offsetWidth = 0
}
const run = () => {
state.percent += 1
isRuning = true
state.percent < 100 ? requestAnimationFrame(run) : (isRuning = false)
}
const cutDone = async () => {
state.loading = true
const url = await uploadCutPhotoToCloud(state.cutImage)
emits('done', url)
state.show = false
handleClose()
}
const edit = () => {
if (!matting.value) return
matting.value.open(state.rawImage, state.cutImage, (base64: string) => {
state.cutImage = base64
state.percent = 0
requestAnimationFrame(run)
})
}
</script>
<style lang="less" scoped>
.uploader {
&__box {
color: #333333;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
}
.content {
position: relative;
display: flex;
justify-content: center;
}
.scan-effect {
position: relative;
height: 50vh;
overflow: hidden;
img {
// width: 100%;
height: 100%;
object-fit: contain;
position: absolute;
}
}
.scan-line {
position: absolute;
top: 0;
width: 1.5px;
height: 100%;
background: rgba(255, 255, 255, 0.7);
// background-image: linear-gradient(to top, transparent, rgba(255, 255, 255, 0.7), transparent);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
.progress {
width: 100%;
}
</style>

View File

@ -0,0 +1,68 @@
/*
* @Author: Jeremy Yu
* @Date: 2024-03-03 19:00:00
* @Description:
* @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-03 19:00:00
*/
import Qiniu from '@/common/methods/QiNiu'
import { TCommonUploadCb, TUploadErrorResult } from "@/api/ai"
import { TImageCutoutState } from "./index.vue"
import api from "@/api"
import { getImage } from '@/common/methods/getImgDetail'
import _config from '@/config'
import { Ref } from 'vue'
/** 选择图片 */
export const selectImageFile = async (
state: TImageCutoutState,
raw: Ref<HTMLElement | null>,
file: File,
successCb?: (result: MediaSource, fileName: string) => void,
uploadCb?: TCommonUploadCb,
) => {
if (file.size > 1024 * 1024 * 2) {
alert('上传图片超出限制')
return false
}
if (!raw.value) return
// 显示选择的图片
raw.value.addEventListener('load', () => {
state.offsetWidth = (raw.value as HTMLElement).offsetWidth
})
state.rawImage = URL.createObjectURL(file)
// 返回抠图结果
const result = await api.ai.upload(file, (up: number, dp: number) => {
uploadCb && uploadCb(up, dp)
if (dp) {
state.progressText = dp === 100 ? '' : '导入中..'
state.progress = dp
} else {
state.progressText = up < 100 ? '上传中..' : '正在处理,请稍候..'
state.progress = up < 100 ? up : 0
}
})
if (typeof result == 'object' && (result as TUploadErrorResult).type !== 'application/json') {
successCb && successCb(result as MediaSource, file.name)
} else alert('服务器繁忙,请稍等下重新尝试~')
}
export async function uploadCutPhotoToCloud(cutImage: string) {
try {
const response = await fetch(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 })
return url
} catch(e) {
console.error(`upload cut file error: msg: ${e}`)
return ''
}
}

View File

@ -1,106 +0,0 @@
<!--
* @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

@ -0,0 +1,136 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-10-08 14:15:17
* @Description: 手动抠图 - 修补擦除
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 09:50:00
-->
<template>
<div>
<el-dialog v-model="state.show" align-center width="90%" @close="state.showMatting = false">
<template #header>
<div class="tool-wrap">
<el-button type="primary" plain @click="done">确认应用</el-button>
<el-radio-group v-model="state.isErasing" style="margin-left: 35px">
<el-radio :value="false" size="large"> <b>修补画笔</b> <i class="icon sd-xiubu" /></el-radio>
<el-radio :value="true" size="large"> <b>擦除画笔</b> <i class="icon sd-cachu" /></el-radio>
</el-radio-group>
<number-slider
v-model="state.radius" class="slider-wrap"
label="画笔尺寸" :showInput="false"
labelWidth="90px"
:maxValue="state.constants?.RADIUS_SLIDER_MAX" :minValue="state.constants?.RADIUS_SLIDER_MIN"
:step="state.constants?.RADIUS_SLIDER_STEP"
/>
<number-slider
v-model="state.hardness" class="slider-wrap"
label="画笔硬度" :showInput="false"
labelWidth="90px"
:maxValue="state.constants?.HARDNESS_SLIDER_MAX" :minValue="state.constants?.HARDNESS_SLIDER_MIN"
:step="state.constants?.HARDNESS_SLIDER_STEP"
/>
</div>
</template>
<matting v-if="state.showMatting" :hasHeader="false" @register="mattingStart" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs, nextTick, DefineComponent } from 'vue'
import matting, { MattingType } from '@palxp/image-extraction'
import { ElRadioGroup, ElRadio } from 'element-plus'
import numberSlider from '@/components/modules/settings/numberSlider.vue'
type TState = {
show: boolean;
showMatting: boolean;
isErasing: boolean;
radius: number | string;
brushSize: string;
hardness: number | string;
hardnessText: string;
constants: MattingType['constants'] | null;
}
type TParams = {
raw: string
result: string
}
type TCallback = ((base64: string) => void) | null
const props: TParams = {
raw: '',
result: ''
}
let callback: TCallback = null //
const state = reactive<TState>({
show: false,
showMatting: false,
isErasing: false,
radius: 0, //
brushSize: '', //
hardness: 0, //
hardnessText: '', //
constants: null,
})
let mattingParam: MattingType | null
const mattingStart = (mattingOptions: MattingType) => {
mattingOptions.initLoadImages(props.raw, props.result)
state.isErasing = mattingOptions.isErasing
state.radius = mattingOptions.radius
state.hardness = mattingOptions.hardness
state.constants = mattingOptions.constants
mattingParam = mattingOptions
}
const open = async (raw: string, result: string, cb: TCallback) => {
state.show = true
props.raw = raw
props.result = result
await nextTick()
setTimeout(() => {
state.showMatting = true
}, 300)
callback = cb
}
defineExpose({
open
})
const done = () => {
state.show = false
callback && callback(mattingParam?.getResult())
}
</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

@ -5,5 +5,5 @@
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-12 00:05:48
*/
import index from './ImageCutout.vue'
import index from './ImageCutout/index.vue'
export default index

View File

@ -2,20 +2,20 @@
* @Author: ShawnPhang
* @Date: 2022-10-08 10:07:19
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 00:04:51
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 18:10:00
-->
<template>
<el-dialog v-model="dialogVisible" title="选择图片" @close="close">
<el-dialog v-model="state.dialogVisible" title="选择图片" @close="close">
<el-tabs tab-position="left" style="height: 60vh" class="demo-tabs" @tab-change="tabChange">
<el-tab-pane label="我的素材">
<div class="pic__box">
<photo-list :isDone="isDone" :listData="imgList" @load="load" @select="selectImg" />
<photo-list :isDone="state.isDone" :listData="state.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)" />
<photo-list :isDone="state.isPicsDone" :listData="state.recommendImgList" @load="loadPic" @select="selectImg($event, state.recommendImgList)" />
</div>
</el-tab-pane>
</el-tabs>
@ -30,96 +30,101 @@
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive } from 'vue'
<script lang="ts" setup>
import { reactive, defineEmits, defineExpose } from 'vue'
import { useStore } from 'vuex'
import { ElTabPane, ElTabs } from 'element-plus'
import { ElTabPane, ElTabs, TabPaneName } from 'element-plus'
import api from '@/api'
import { TGetImageListResult } from '@/api/material'
export default defineComponent({
components: { [ElTabPane.name]: ElTabPane, [ElTabs.name]: ElTabs },
emits: ['select'],
setup(props, context) {
const store = useStore()
const state: any = reactive({
dialogVisible: false,
imgList: [],
recommendImgList: [],
isDone: false,
isPicsDone: false,
})
type TEmits = (event: 'select', data: TGetImageListResult) => void
let loading = false
let page = 0
let picPage = 0
const load = (init?: boolean) => {
if (init) {
state.imgList = []
page = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
page += 1
api.material.getMyPhoto({ page }).then(({ list }: any) => {
list.length <= 0 ? (state.isDone = true) : (state.imgList = state.imgList.concat(list))
setTimeout(() => {
loading = false
}, 100)
})
}
const 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)
})
}
type TState = {
dialogVisible: boolean;
imgList: TGetImageListResult[];
recommendImgList: TGetImageListResult[];
isDone: boolean;
isPicsDone: boolean;
}
const open = () => {
state.dialogVisible = true
load()
store.commit('setShowMoveable', false)
}
const emits = defineEmits<TEmits>()
const close = () => {
store.commit('setShowMoveable', true)
}
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,
}
},
const store = useStore()
const state = reactive<TState>({
dialogVisible: false,
imgList: [],
recommendImgList: [],
isDone: false,
isPicsDone: false,
})
let loading = false
let page = 0
let picPage = 0
const load = async (init?: boolean) => {
if (init) {
state.imgList = []
page = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
page += 1
api.material.getMyPhoto({ page }).then(({ list }) => {
list.length <= 0 ? (state.isDone = true) : (state.imgList = state.imgList.concat(list))
setTimeout(() => {
loading = false
}, 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 }) => {
list.length <= 0 ? (state.isPicsDone = true) : (state.recommendImgList = state.recommendImgList.concat(list))
setTimeout(() => {
loading = false
}, 100)
})
}
const open = () => {
state.dialogVisible = true
load()
store.commit('setShowMoveable', false)
}
const close = () => {
store.commit('setShowMoveable', true)
}
const selectImg = (index: number, list: TGetImageListResult[]) => {
const item: TGetImageListResult = list ? list[index] : state.imgList[index]
// context.emit('select', item)
emits('select', item)
state.dialogVisible = false
}
const tabChange = (index: TabPaneName) => {
if (index == 1) {
loadPic(true)
}
}
defineExpose({
open
})
</script>
<style lang="less" scoped>

View File

@ -2,108 +2,63 @@
* @Author: ShawnPhang
* @Date: 2022-03-16 09:15:52
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-31 15:43:10
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 18:50:00
-->
<template>
<div ref="qrCodeDom" class="qrcode__wrap"></div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch, nextTick } from 'vue'
import QRCodeStyling, { DrawType, TypeNumber, Mode, ErrorCorrectionLevel, DotType, CornerSquareType, CornerDotType, Extension } from 'qr-code-styling'
<script lang="ts" setup>
import { onMounted, ref, watch, nextTick, defineProps } from 'vue'
import QRCodeStyling, {DotType, Options } from 'qr-code-styling'
import { debounce } from 'throttle-debounce'
import { generateOption } from './method'
export default defineComponent({
props: {
width: {
default: 300,
},
height: {
default: 300,
},
image: {},
value: {},
dotsOptions: {
default: () => {
return {}
},
},
},
setup(props) {
let options = {}
watch(
() => [props.width, props.height, props.dotsOptions],
() => {
render()
},
)
export type TQrcodeProps = {
width?: number
height?: number
image?: string
value?: string
dotsOptions: {
color: string,
type: DotType,
}
}
const render = debounce(300, false, async () => {
options = {
width: props.width,
height: props.height,
type: 'canvas' as DrawType, // canvas svg
data: props.value,
image: props.image, // /favicon.svg
margin: 0,
qrOptions: {
typeNumber: 3 as TypeNumber,
mode: 'Byte' as Mode,
errorCorrectionLevel: 'M' as ErrorCorrectionLevel,
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 6,
crossOrigin: 'anonymous',
},
backgroundOptions: {
color: '#ffffff',
},
dotsOptions: {
color: '#41b583',
type: 'rounded' as DotType,
...props.dotsOptions,
},
cornersSquareOptions: {
color: props.dotsOptions.color,
type: '',
// type: 'extra-rounded' as CornerSquareType,
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#25456e' }, { offset: 1, color: '#4267b2' }]
// },
},
cornersDotOptions: {
color: props.dotsOptions.color,
type: 'square' as CornerDotType,
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#00266e' }, { offset: 1, color: '#4060b3' }]
// },
},
}
if (props.value) {
qrCode.update(options)
await nextTick()
qrCodeDom.value.firstChild.style = 'width: 100%;' //
}
})
const qrCode = new QRCodeStyling(options)
const qrCodeDom = ref<HTMLElement>()
onMounted(() => {
render()
qrCode.append(qrCodeDom.value)
})
return {
qrCodeDom,
}
},
const props = withDefaults(defineProps<TQrcodeProps>(), {
width: 300,
height: 300,
dotsOptions: () => ({
color: '#41b583',
type: 'rounded',
})
})
let options: Options = {}
watch(
() => [props.width, props.height, props.dotsOptions],
() => {
render()
},
)
const render = debounce(300, false, async () => {
options = generateOption(props)
if (props.value) {
options && qrCode.update(options)
await nextTick()
if (!qrCodeDom?.value?.firstChild) return
(qrCodeDom.value.firstChild as HTMLElement).setAttribute('style', "width: 100%;") //
}
})
const qrCode = new QRCodeStyling(options)
const qrCodeDom = ref<HTMLElement>()
onMounted(() => {
render()
qrCode.append(qrCodeDom.value)
})
</script>

View File

@ -0,0 +1,60 @@
/*
* @Author: Jeremy Yu
* @Date: 2024-03-04 18:10:00
* @Description:
* @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 18:10:00
*/
import { CornerDotType, Options } from "qr-code-styling"
import { TQrcodeProps } from "./index.vue"
/** 生成二维码数据 */
export function generateOption(props: TQrcodeProps): Options {
return {
width: props.width,
height: props.height,
type: 'canvas', // canvas svg
data: props.value,
image: props.image, // /favicon.svg
margin: 0,
qrOptions: {
typeNumber: 3,
mode: 'Byte',
errorCorrectionLevel: 'M',
},
imageOptions: {
hideBackgroundDots: true,
imageSize: 0.4,
margin: 6,
crossOrigin: 'anonymous',
},
backgroundOptions: {
color: '#ffffff',
},
dotsOptions: {
// color: '#41b583',
// type: 'rounded' as DotType,
...props.dotsOptions,
},
cornersSquareOptions: {
color: props.dotsOptions.color,
// type: '',
// type: 'extra-rounded' as CornerSquareType,
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#25456e' }, { offset: 1, color: '#4267b2' }]
// },
},
cornersDotOptions: {
color: props.dotsOptions.color,
type: 'square' as CornerDotType,
// gradient: {
// type: 'linear', // 'radial'
// rotation: 180,
// colorStops: [{ offset: 0, color: '#00266e' }, { offset: 1, color: '#4060b3' }]
// },
},
}
}

View File

@ -1,136 +1,142 @@
<template>
<div v-show="showMenuBg" id="menu-bg" class="menu-bg" @click="closeMenu">
<ul ref="menuList" class="menu-list" :style="styleObj">
<li v-for="(item, index) in menuList.list" :key="index" :class="{ 'menu-item': true, 'disable-menu': dCopyElement.length === 0 && item.type === 'paste' }" @click.stop="selectMenu(item.type)">
<li v-for="(item, index) in menuListData.list" :key="index" :class="{ 'menu-item': true, 'disable-menu': dCopyElement.length === 0 && item.type === 'paste' }" @click.stop="selectMenu(item.type)">
{{ item.text }}
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { mapGetters, mapActions } from 'vuex'
import { widgetMenu, pageMenu, menuList } from './rcMenuData'
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useStore } from 'vuex'
import {
widgetMenu as widget,
pageMenu as page,
menuList as menu,
TMenuItemData, TWidgetItemData,
} from './rcMenuData'
import { getTarget } from '@/common/methods/target'
import { useSetupMapGetters } from '@/common/hooks/mapGetters';
export default defineComponent({
setup() {},
data() {
return {
menuList,
showMenuBg: false,
widgetMenu,
pageMenu,
}
},
computed: {
...mapGetters(['dActiveElement', 'dAltDown', 'dWidgets', 'dCopyElement']),
styleObj() {
return {
left: this.menuList.left + 'px',
top: this.menuList.top + 'px',
const store = useStore()
const menuListData = ref<TMenuItemData>({...menu})
const showMenuBg = ref<boolean>(false)
const widgetMenu = ref<TWidgetItemData[]>({...widget})
const pageMenu = ref<TWidgetItemData[]>({...page})
const {dActiveElement, dAltDown, dWidgets, dCopyElement} = useSetupMapGetters(['dActiveElement', 'dAltDown', 'dWidgets', 'dCopyElement'])
const styleObj = computed(() => {
return {
left: menuListData.value.left + 'px',
top: menuListData.value.top + 'px',
}
})
onMounted(() => {
document.oncontextmenu = mouseRightClick
})
async function mouseRightClick(e: MouseEvent) {
e.stopPropagation()
e.preventDefault()
if (showMenuBg.value) {
showMenuBg.value = false
return
}
if (!e.target) return
let target = await getTarget(e.target as HTMLElement)
if (!target) return
let type = target.getAttribute('data-type')
if (type) {
let uuid = target.getAttribute('data-uuid') //
if (uuid !== '-1' && !dAltDown) {
let widget = dWidgets.value.find((item: any) => item.uuid === uuid)
if (
widget.parent !== '-1' &&
widget.parent !== dActiveElement.value.uuid &&
widget.parent !== dActiveElement.value.parent
) {
uuid = widget.parent
}
},
},
mounted() {
document.oncontextmenu = this.mouseRightClick
},
methods: {
...mapActions(['selectWidget', 'copyWidget', 'pasteWidget', 'updateLayerIndex', 'deleteWidget', 'ungroup']),
async mouseRightClick(e: any) {
e.stopPropagation()
e.preventDefault()
if (this.showMenuBg) {
this.showMenuBg = false
}
store.dispatch('selectWidget', {
uuid: uuid ?? '-1',
})
showMenu(e)
}
}
function showMenu(e: MouseEvent) {
let isPage = dActiveElement.value.uuid === '-1'
menuListData.value.list = isPage ? pageMenu.value : widgetMenu.value
if (dActiveElement.value.isContainer) {
let ungroup: TWidgetItemData[] = [
{
type: 'ungroup',
text: '取消组合',
},
]
menuListData.value.list = ungroup.concat(menuListData.value.list)
}
showMenuBg.value = true
// document.getElementById('menu-bg').addEventListener('click', this.closeMenu, false)
let mx = e.pageX
let my = e.pageY
let listWidth = 120
if (mx + listWidth > window.innerWidth) {
mx -= listWidth
}
let listHeight = (14 + 10) * menuListData.value.list.length + 10
if (my + listHeight > window.innerHeight) {
my -= listHeight
}
menuListData.value.left = mx
menuListData.value.top = my
}
function closeMenu() {
showMenuBg.value = false
}
/** 点击菜单触发事件 */
function selectMenu(type: TWidgetItemData['type']) {
switch (type) {
case 'copy':
store.dispatch('copyWidget')
break
case 'paste':
if (dCopyElement.value.length === 0) {
return
}
// let target = e.target
let target = await getTarget(e.target)
store.dispatch('pasteWidget')
break
case 'index-up':
store.dispatch('updateLayerIndex', {
uuid: dActiveElement.value.uuid,
value: 1,
isGroup: dActiveElement.value.isContainer,
})
break
case 'index-down':
store.dispatch('updateLayerIndex', {
uuid: dActiveElement.value.uuid,
value: -1,
isGroup: dActiveElement.value.isContainer,
})
break
case 'del':
store.dispatch('deleteWidget')
break
case 'ungroup':
store.dispatch('ungroup', dActiveElement.value.uuid)
break
}
closeMenu()
}
let type = target.getAttribute('data-type')
if (type) {
let uuid = target.getAttribute('data-uuid') //
if (uuid !== '-1' && !this.dAltDown) {
let widget = this.dWidgets.find((item: any) => item.uuid === uuid)
if (widget.parent !== '-1' && widget.parent !== this.dActiveElement.uuid && widget.parent !== this.dActiveElement.parent) {
uuid = widget.parent
}
}
this.selectWidget({
uuid: uuid || '-1',
})
this.showMenu(e)
}
},
showMenu(e: any) {
let isPage = this.dActiveElement.uuid === '-1'
this.menuList.list = isPage ? this.pageMenu : this.widgetMenu
if (this.dActiveElement.isContainer) {
let ungroup = [
{
type: 'ungroup',
text: '取消组合',
},
]
this.menuList.list = ungroup.concat(this.menuList.list)
}
this.showMenuBg = true
// document.getElementById('menu-bg').addEventListener('click', this.closeMenu, false)
let mx = e.pageX
let my = e.pageY
let listWidth = 120
if (mx + listWidth > window.innerWidth) {
mx -= listWidth
}
let listHeight = (14 + 10) * this.menuList.list.length + 10
if (my + listHeight > window.innerHeight) {
my -= listHeight
}
this.menuList.left = mx
this.menuList.top = my
},
closeMenu() {
this.showMenuBg = false
// document.getElementById('menu-bg').removeEventListener('click', this.closeMenu, false)
},
selectMenu(type) {
switch (type) {
case 'copy':
this.copyWidget()
break
case 'paste':
if (this.dCopyElement.length === 0) {
return
}
this.pasteWidget()
break
case 'index-up':
this.updateLayerIndex({
uuid: this.dActiveElement.uuid,
value: 1,
isGroup: this.dActiveElement.isContainer,
})
break
case 'index-down':
this.updateLayerIndex({
uuid: this.dActiveElement.uuid,
value: -1,
isGroup: this.dActiveElement.isContainer,
})
break
case 'del':
this.deleteWidget()
break
case 'ungroup':
this.ungroup(this.dActiveElement.uuid)
break
}
this.closeMenu()
},
},
})
</script>
<style lang="less" scoped>

View File

@ -2,15 +2,28 @@
* @Author: ShawnPhang
* @Date: 2021-07-30 17:38:50
* @Description:
* @LastEditors: ShawnPhang
* @LastEditTime: 2021-07-30 18:15:22
* @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 18:50:00
*/
export const menuList: any = {
export type TMenuItemData = {
left: number
top: number
list: TWidgetItemData[]
}
export const menuList: TMenuItemData = {
left: 0,
top: 0,
list: [],
}
export const widgetMenu = [
export type TWidgetItemData = {
type: 'copy' | 'paste' | 'index-up' | 'index-down' | 'del' | 'ungroup'
text: string
}
export const widgetMenu: TWidgetItemData[] = [
{
type: 'copy',
text: '复制',
@ -33,7 +46,7 @@ export const widgetMenu = [
},
]
export const pageMenu = [
export const pageMenu: TWidgetItemData[] = [
{
type: 'paste',
text: '粘贴',

View File

@ -2,66 +2,63 @@
* @Author: ShawnPhang
* @Date: 2021-08-01 11:12:17
* @Description: 前端出图 - 用于封面
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-13 17:36:36
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-04 18:50:00
-->
<template>
<div id="cover-wrap"></div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, watch, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { mapGetters, mapActions } from 'vuex'
<script lang="ts" setup>
import { useStore } from 'vuex'
import html2canvas from 'html2canvas'
import api from '@/api'
import Qiniu from '@/common/methods/QiNiu'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
export default defineComponent({
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, context) {
const { proxy }: any = getCurrentInstance() as ComponentInternalInstance
const store = useStore();
async function createCover(cb: any) {
const nowZoom = proxy?.dZoom
//
proxy?.selectWidget({
uuid: '-1',
})
proxy?.updateZoom(100)
const opts = {
useCORS: true, //
scale: 0.2,
}
setTimeout(async () => {
const clonePage: HTMLElement = document.getElementById('page-design-canvas').cloneNode(true)
clonePage.setAttribute('id', 'clone-page')
document.body.appendChild(clonePage)
html2canvas(document.getElementById('clone-page'), opts).then((canvas: any) => {
canvas.toBlob(
async (blobObj: Blob) => {
const result: any = await Qiniu.upload(blobObj, { bucket: 'xp-design', prePath: 'cover/user' })
cb(result)
},
'image/jpeg',
0.15,
)
proxy?.updateZoom(nowZoom)
clonePage.remove()
})
}, 10)
}
const { dZoom } = useSetupMapGetters(['dZoom'])
return {
createCover,
}
},
computed: {
...mapGetters(['dZoom']),
},
methods: {
...mapActions(['selectWidget', 'updateZoom']),
},
// props: ['modelValue'],
// emits: ['update:modelValue'],
async function createCover(cb: any) {
const nowZoom = dZoom.value
//
store.dispatch('selectWidget', {
uuid: '-1',
})
store.dispatch('updateZoom', 100)
const opts = {
useCORS: true, //
scale: 0.2,
}
setTimeout(async () => {
const clonePage = document.getElementById('page-design-canvas')?.cloneNode(true) as HTMLElement
if (!clonePage) return
clonePage.setAttribute('id', 'clone-page')
document.body.appendChild(clonePage)
html2canvas(clonePage, opts).then((canvas) => {
canvas.toBlob(
async (blobObj) => {
if (blobObj) {
const result = await Qiniu.upload(blobObj, { bucket: 'xp-design', prePath: 'cover/user' })
cb(result)
}
},
'image/jpeg',
0.15,
)
store.dispatch('updateZoom', nowZoom)
clonePage.remove()
})
}, 10)
}
defineExpose({
createCover
})
</script>

View File

@ -13,28 +13,21 @@
</el-popover>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { defineProps } from 'vue'
export default defineComponent({
props: {
title: {
default: '',
},
width: {
default: 0,
},
content: {
default: '',
},
// top/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-end
position: {
default: 'bottom',
},
// offset: {
// default: 0,
// },
},
setup() {},
type TProps = {
title: string
width: number
content: string
position?: "bottom" | "auto" | "auto-start" | "auto-end" | "top" | "right" | "left" | "top-start" | "top-end" | "bottom-start" | "bottom-end" | "right-start" | "right-end" | "left-start" | "left-end"
offset?: number
}
const { title, width, content, position } = withDefaults(defineProps<TProps>(), {
title: '',
width: 0,
content: '',
position: 'bottom'
})
</script>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-12-28 09:29:42
* @Description: 百分比进度条
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-07-13 23:05:29
* @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-05 10:50:00
-->
<template>
<div v-if="percent" class="mask">
@ -16,33 +16,45 @@
</div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
<script lang="ts" setup>
import { watch, defineProps, defineEmits } from 'vue'
import { ElProgress } from 'element-plus'
export default defineComponent({
components: { ElProgress },
props: ['percent', 'text', 'cancelText', 'msg'],
emits: ['done', 'cancel'],
setup(props, context) {
watch(
() => props.percent,
(num) => {
if (num >= 100) {
setTimeout(() => {
context.emit('done')
}, 1000)
}
},
)
type TProps = {
percent: number
text: string
cancelText: string
msg?: string
}
const cancel = () => {
context.emit('cancel')
type TEmits = {
(event: 'done'): void
(event: 'cancel'): void
}
const {percent, text, cancelText, msg} = defineProps<TProps>()
const emit = defineEmits<TEmits>()
watch(
() => percent,
(num) => {
if (num >= 100) {
setTimeout(() => {
emit('done')
}, 1000)
}
return { cancel }
},
)
const cancel = () => {
emit('cancel')
}
defineExpose({
cancel
})
</script>
<style lang="less" scoped>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2021-08-29 18:17:13
* @Description: 二次封装上传组件
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-05 15:46:02
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-05 10:50:00
-->
<template>
<el-upload action="" accept="image/*" :http-request="upload" :show-file-list="false" multiple>
@ -13,115 +13,135 @@
</el-upload>
</template>
<script lang="ts">
import { defineComponent, onMounted, nextTick } from 'vue'
import { ElUpload } from 'element-plus'
<script lang="ts" setup>
import { onMounted, nextTick, defineProps, withDefaults, defineEmits } from 'vue'
import { ElUpload, UploadRequestOptions } from 'element-plus'
import Qiniu from '@/common/methods/QiNiu'
import { getImage } from '@/common/methods/getImgDetail'
import _config from '@/config'
import useNotification from '@/common/methods/notification'
export default defineComponent({
components: { ElUpload },
props: {
modelValue: {},
options: {
default: () => {
return { bucket: 'xp-design', prePath: 'user' }
},
},
hold: {
default: false, //
},
},
emits: ['done', 'update:modelValue', 'load'],
setup(props, context) {
let uploading: boolean = false // Flag
let timer: any = null
type TModelData = {
num?: string | number
ratio?: string
}
let uploadList: any[] = [] //
let index: number = 0 //
let count: number = 0 //
export type TUploadDoneData = {
width: number
height: number
url: string
}
let tempSimpleRes: any = null //
type TQiNiuUploadReturn = { hash: string, key: string }
onMounted(async () => {
await nextTick()
setTimeout(() => {
//
const link_element = document.createElement('script')
link_element.setAttribute('src', _config.QINIUYUN_PLUGIN)
document.head.appendChild(link_element)
}, 1000)
})
type TProps = {
modelValue?: TModelData
options?: { bucket: string, prePath: string }
hold?: boolean
}
const upload = ({ file }: any) => {
if (props.hold) {
context.emit('load', file)
return
}
uploadList.push(file)
clearTimeout(timer)
count++
type TEmits = {
(event: 'done', data: TUploadDoneData): void
(event: 'update:modelValue', data: TModelData): void
(event: 'load', data: File): void
}
const props = withDefaults(defineProps<TProps>(), {
modelValue: () => ({}),
options: () => ({ bucket: 'xp-design', prePath: 'user' }),
hold: false
})
const emit = defineEmits<TEmits>()
let uploading: boolean = false // Flag
let timer: number
let uploadList: File[] = [] //
let index: number = 0 //
let count: number = 0 //
let tempSimpleRes: TQiNiuUploadReturn | null //
onMounted(async () => {
await nextTick()
setTimeout(() => {
//
const link_element = document.createElement('script')
link_element.setAttribute('src', _config.QINIUYUN_PLUGIN)
document.head.appendChild(link_element)
}, 1000)
})
const upload = async ({ file }: UploadRequestOptions) => {
if (props.hold) {
emit('load', file)
return
}
uploadList.push(file)
clearTimeout(timer)
count++
updatePercent(null)
uploadQueue()
}
//
const uploadQueue = async () => {
if (!uploading) {
uploading = true
const file = uploadList[0]
if (file) {
if (file.size <= 1024 * 1024) {
tempSimpleRes = await qiNiuUpload(file) //
console.log("tempSimpleRes", tempSimpleRes)
const { width, height } = await getImage(file)
useNotification('上传成功', '公共测试账户,上传请注意保护隐私哦!', { position: 'bottom-left' })
emit('done', { width, height, url: _config.IMG_URL + tempSimpleRes?.key }) //
} else useNotification('爱护小水管', '请上传小于 1M 的图片哦!', { type: 'error', position: 'bottom-left' })
uploading = false
handleRemove() //
index++
updatePercent(null)
uploadQueue()
} else {
uploading = false
timer = setTimeout(() => {
index = count = 0
updatePercent(0)
}, 3000)
}
//
const uploadQueue = async () => {
if (!uploading) {
uploading = true
const file = uploadList[0]
if (file) {
if (file.size <= 1024 * 1024) {
tempSimpleRes = await qiNiuUpload(file) //
const { width, height }: any = await getImage(file)
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
handleRemove() //
index++
updatePercent(null)
uploadQueue()
} else {
uploading = false
timer = setTimeout(() => {
index = count = 0
updatePercent(0)
}, 3000)
}
}
}
const qiNiuUpload = async (file: File) => {
updatePercent(0)
return new Promise(async (resolve: Function) => {
if (props.hold) {
context.emit('load', file)
resolve()
} else {
const result: any = await Qiniu.upload(file, props.options, (res: Type.Object) => {
updatePercent(res.total.percent)
})
resolve(result)
}
})
}
//
const updatePercent = (p?: number | null) => {
const num = typeof p === 'number' ? String(p) : p
const percent = { ...props.modelValue }
percent.num = num ? Number(num).toFixed(0) : percent.num
percent.ratio = count ? `${index} / ${count}` : ''
context.emit('update:modelValue', percent)
}
const handleRemove = () => {
uploadList.length > 0 && uploadList.splice(0, 1)
}
}
}
return {
upload,
const qiNiuUpload = async (file: File): Promise<null | TQiNiuUploadReturn> => {
updatePercent(0)
return new Promise(async (resolve) => {
if (props.hold) {
emit('load', file)
resolve(null)
} else {
const result = await Qiniu.upload(file, props.options, (res: Type.Object) => {
updatePercent(res.total.percent)
})
resolve(result)
}
},
})
}
//
const updatePercent = (p?: number | null) => {
const num = typeof p === 'number' ? String(p) : p
const percent = { ...props.modelValue }
percent.num = num ? Number(num).toFixed(0) : percent.num
percent.ratio = count ? `${index} / ${count}` : ''
emit('update:modelValue', percent)
}
const handleRemove = () => {
uploadList.length > 0 && uploadList.splice(0, 1)
}
defineExpose({
upload,
})
</script>

View File

@ -5,6 +5,8 @@
* @LastEditors: ShawnPhang
* @LastEditTime: 2022-04-08 10:28:47
*/
import { App } from "vue"
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
@ -15,9 +17,10 @@ const exclude = ['settings', 'layout']
const regex = RegExp('.*^(?!.*?(' + exclude.join('|') + ')).*\\.vue$')
// const requireComponent = require.context('.', true, /\.vue$/) // 找到components文件夹下以.vue命名的文件
const requireComponent = import.meta.globEager('./**/*.vue')
function guide(Vue: Type.Object) {
const requireComponent = import.meta.glob('./**/*.vue', { eager: true })
function guide(Vue: App) {
for (const fileName in requireComponent) {
if (regex.test(fileName)) {
const componentConfig = requireComponent[fileName]

View File

@ -1,304 +0,0 @@
<template>
<div id="page-design" ref="page-design" :style="{ paddingTop: dPaddingTop + 'px' }">
<div
id="out-page"
class="out-page"
:style="{
width: (dPage.width * dZoom) / 100 + 120 + 'px',
height: (dPage.height * dZoom) / 100 + 120 + 'px',
opacity: 1 - (dZoom < 100 ? dPage.tag : 0),
}"
>
<slot />
<div
:id="pageDesignCanvasId"
class="design-canvas"
:data-type="dPage.type"
:data-uuid="dPage.uuid"
:style="{
width: dPage.width + 'px',
height: dPage.height + 'px',
transform: 'scale(' + dZoom / 100 + ')',
transformOrigin: (dZoom >= 100 ? 'center' : 'left') + ' top',
backgroundColor: dPage.backgroundColor,
backgroundImage: `url(${dPage?.backgroundImage})`,
backgroundSize: dPage?.backgroundTransform?.x ? 'auto' : 'cover',
backgroundPositionX: (dPage?.backgroundTransform?.x || 0) + 'px',
backgroundPositionY: (dPage?.backgroundTransform?.y || 0) + 'px',
opacity: dPage.opacity + (dZoom < 100 ? dPage.tag : 0),
}"
@mousemove="dropOver($event)"
@drop="drop($event)"
@mouseup="drop($event)"
>
<!-- <grid-size /> -->
<!-- :class="{
layer: true,
'layer-active': getIsActive(layer.uuid),
'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid,
}" -->
<component :is="layer.type" v-for="layer in getlayers()" :id="layer.uuid" :key="layer.uuid" :class="['layer', { 'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid, 'layer-no-hover': dActiveElement.uuid === layer.uuid }]" :data-title="layer.type" :params="layer" :parent="dPage" :data-type="layer.type" :data-uuid="layer.uuid">
<template v-if="layer.isContainer">
<!-- :class="{
layer: true,
'layer-active': getIsActive(widget.uuid),
'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent,
'layer-hover': widget.uuid === dHoverUuid,
}" -->
<component :is="widget.type" v-for="widget in getChilds(layer.uuid)" :key="widget.uuid" child :class="['layer', { 'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent }]" :data-title="widget.type" :params="widget" :parent="layer" :data-type="widget.type" :data-uuid="widget.uuid" />
</template>
</component>
<!-- <ref-line v-if="dSelectWidgets.length === 0" /> -->
<!-- <size-control v-if="dSelectWidgets.length === 0" /> -->
</div>
</div>
</div>
</template>
<script>
import { defineComponent, nextTick } from 'vue'
import { mapGetters, mapActions } from 'vuex'
import { getTarget } from '@/common/methods/target'
import setWidgetData from '@/common/methods/DesignFeatures/setWidgetData'
import PointImg from '@/utils/plugins/pointImg'
import getComponentsData from '@/common/methods/DesignFeatures/setComponents'
import { debounce } from 'throttle-debounce'
//
const NAME = 'page-design'
import { move, moveInit } from '@/mixins/move'
export default defineComponent({
name: NAME,
// components: {lineGuides},
mixins: [moveInit],
props: ['pageDesignCanvasId'],
data() {
return {}
},
computed: {
...mapGetters(['dPaddingTop', 'dPage', 'dZoom', 'dScreen', 'dWidgets', 'dActiveElement', 'dHoverUuid', 'dSelectWidgets', 'dAltDown', 'dDraging', 'showRotatable']),
},
mounted() {
this.getScreen()
document.getElementById('page-design').addEventListener('mousedown', this.handleSelection, false)
document.getElementById('page-design').addEventListener('mousemove', debounce(100, false, this.handleMouseMove), false)
},
beforeUnmount() {},
methods: {
...mapActions(['updateScreen', 'selectWidget', 'deleteWidget', 'addWidget', 'addGroup']),
// 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
}
e.preventDefault()
let { data, type } = this.$store.getters.selectItem
if (type !== 'image') {
return
}
const target = await getTarget(e.target)
const uuid = target.getAttribute('data-uuid')
this.$store.dispatch('setDropOver', uuid)
if (e.target.getAttribute('putIn')) {
this._dropIn = uuid
const imgUrl = data.value.thumb || data.value.url
!this._srcCache && (this._srcCache = target.firstElementChild.firstElementChild.src)
target.firstElementChild.firstElementChild.src = imgUrl
} else {
this._srcCache && (target.firstElementChild.firstElementChild.src = this._srcCache)
this._srcCache = ''
this._dropIn = ''
}
},
async drop(e) {
if (!this.dDraging) {
return
}
this.$store.commit('setDraging', false)
const dropIn = this._dropIn
this._dropIn = ''
this.$store.dispatch('setDropOver', '-1')
this.$store.commit('setShowMoveable', false) //
let lost = e.target.className !== 'design-canvas' // className === 'design-canvas' , id: "page-design-canvas"
// e.stopPropagation()
e.preventDefault()
let { data: item, type } = JSON.parse(JSON.stringify(this.$store.getters.selectItem))
//
this.$store.commit('selectItem', {})
let setting = {}
if (!type) {
return
}
//
setting = await setWidgetData(type, item, setting)
//
const lostX = e.x - document.getElementById('page-design-canvas').getBoundingClientRect().left
const lostY = e.y - document.getElementById('page-design-canvas').getBoundingClientRect().top
//
if (type === 'group') {
let parent = {}
item = await getComponentsData(item)
item.forEach((element) => {
if (element.type === 'w-group') {
parent.width = element.width
parent.height = element.height
}
})
const half = { x: parent.width ? (parent.width * this.$store.getters.dZoom) / 100 / 2 : 0, y: parent.height ? (parent.height * this.$store.getters.dZoom) / 100 / 2 : 0 }
item.forEach((element) => {
element.left += (lost ? lostX - half.x : e.layerX - half.x) * (100 / this.$store.getters.dZoom)
element.top += (lost ? lostY - half.y : e.layerY - half.y) * (100 / this.$store.getters.dZoom)
})
this.addGroup(item)
}
//
const half = { x: setting.width ? (setting.width * this.$store.getters.dZoom) / 100 / 2 : 0, y: setting.height ? (setting.height * this.$store.getters.dZoom) / 100 / 2 : 0 }
// const half = { x: (this.dDragInitData.offsetX * this.dZoom) / 100, y: (this.dDragInitData.offsetY * this.dZoom) / 100 }
setting.left = (lost ? lostX - half.x : e.layerX - half.x) * (100 / this.$store.getters.dZoom)
setting.top = (lost ? lostY - half.y : e.layerY - half.y) * (100 / this.$store.getters.dZoom)
if (lost && type === 'image') {
// svg
const target = await getTarget(e.target)
const targetType = target.getAttribute('data-type')
const uuid = target.getAttribute('data-uuid')
if (targetType === 'w-mask') {
//
this.$store.commit('setShowMoveable', true) //
const widget = this.dWidgets.find((item) => item.uuid === uuid)
widget.imgUrl = item.value.url
// if (e.target.className.baseVal) {
// !widget.imgs && (widget.imgs = {})
// widget.imgs[`${e.target.className.baseVal}`] = item.value.url
// }
} else {
if (dropIn) {
const widget = this.dWidgets.find((item) => item.uuid == dropIn)
widget.imgUrl = item.value.url
console.log('加入+', widget)
this.$store.commit('setShowMoveable', true) //
} else {
this.addWidget(setting) //
}
}
} else if (type === 'bg') {
console.log('背景图片放置')
} else if (type !== 'group') {
console.log(setting)
this.addWidget(setting) //
}
//
// this.$store.commit('selectItem', {})
},
getScreen() {
let screen = this.$refs['page-design']
this.updateScreen({
width: screen.offsetWidth,
height: screen.offsetHeight,
})
},
async handleMouseMove(e) {
const pImg = new PointImg(e.target)
const { rgba } = pImg.getColorXY(e.offsetX, e.offsetY)
if (rgba && rgba === 'rgba(0,0,0,0)') {
console.log('解析点位颜色: ', rgba)
let target = await getTarget(e.target)
target.style.pointerEvents = 'none'
setTimeout(() => {
target.style.pointerEvents = 'auto'
}, 300)
}
},
async handleSelection(e) {
if (e.which === 3) {
return
}
let target = await getTarget(e.target)
let type = target.getAttribute('data-type')
if (type) {
let uuid = target.getAttribute('data-uuid')
if (uuid !== '-1' && !this.dAltDown) {
let widget = this.dWidgets.find((item) => item.uuid === uuid)
if (widget.parent !== '-1' && widget.parent !== this.dActiveElement.uuid && widget.parent !== this.dActiveElement.parent) {
uuid = widget.parent
}
}
//
// this.$store.commit('setMoveable', false)
if (this.showRotatable !== false) {
this.selectWidget({
uuid: uuid,
})
}
if (uuid !== '-1') {
this.initmovement && this.initmovement(e) // mixins
}
} else {
//
this.selectWidget({
uuid: '-1',
})
}
},
getlayers() {
return this.dWidgets.filter((item) => item.parent === this.dPage.uuid)
},
getChilds(uuid) {
return this.dWidgets.filter((item) => item.parent === uuid)
},
// getIsActive(uuid) {
// if (this.dSelectWidgets.length > 0) {
// let widget = this.dSelectWidgets.find((item) => item.uuid === uuid)
// if (widget) {
// return true
// }
// return false
// } else {
// return uuid === this.dActiveElement.uuid
// }
// },
},
})
</script>
<style lang="less" scoped>
#page-design {
height: 100%;
// display: flex;
// align-items: center;
overflow: auto;
position: relative;
width: 100%;
.out-page {
margin: 0 auto;
padding: 60px;
position: relative;
.design-canvas {
// transition: all 0.3s;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
box-shadow: 1px 1px 10px 3px rgba(0, 0, 0, 0.1);
margin: 0 auto;
position: relative;
// z-index: -9999;
// overflow: hidden;
// overflow: auto;
}
// .design-canvas ::-webkit-scrollbar {
// display: none; /* Chrome Safari */
// }
}
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<div id="page-design" ref="page_design" :style="{ paddingTop: dPaddingTop + 'px' }">
<div
id="out-page"
class="out-page"
:style="{
width: (dPage.width * dZoom) / 100 + 120 + 'px',
height: (dPage.height * dZoom) / 100 + 120 + 'px',
opacity: 1 - (dZoom < 100 ? dPage.tag : 0),
}"
>
<slot />
<div
:id="pageDesignCanvasId"
class="design-canvas"
:data-type="dPage.type"
:data-uuid="dPage.uuid"
:style="{
width: dPage.width + 'px',
height: dPage.height + 'px',
transform: 'scale(' + dZoom / 100 + ')',
transformOrigin: (dZoom >= 100 ? 'center' : 'left') + ' top',
backgroundColor: dPage.backgroundColor,
backgroundImage: `url(${dPage?.backgroundImage})`,
backgroundSize: dPage?.backgroundTransform?.x ? 'auto' : 'cover',
backgroundPositionX: (dPage?.backgroundTransform?.x || 0) + 'px',
backgroundPositionY: (dPage?.backgroundTransform?.y || 0) + 'px',
opacity: dPage.opacity + (dZoom < 100 ? dPage.tag : 0),
}"
@mousemove="dropOver($event)"
@drop="drop($event)"
@mouseup="drop($event)"
>
<!-- <grid-size /> -->
<!-- :class="{
layer: true,
'layer-active': getIsActive(layer.uuid),
'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid,
}" -->
<component :is="layer.type" v-for="layer in getlayers()" :id="layer.uuid" :key="layer.uuid" :class="['layer', { 'layer-hover': layer.uuid === dHoverUuid || dActiveElement.parent === layer.uuid, 'layer-no-hover': dActiveElement.uuid === layer.uuid }]" :data-title="layer.type" :params="layer" :parent="dPage" :data-type="layer.type" :data-uuid="layer.uuid">
<template v-if="layer.isContainer">
<!-- :class="{
layer: true,
'layer-active': getIsActive(widget.uuid),
'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent,
'layer-hover': widget.uuid === dHoverUuid,
}" -->
<component :is="widget.type" v-for="widget in getChilds(layer.uuid)" :key="widget.uuid" child :class="['layer', { 'layer-no-hover': dActiveElement.uuid !== widget.parent && dActiveElement.parent !== widget.parent }]" :data-title="widget.type" :params="widget" :parent="layer" :data-type="widget.type" :data-uuid="widget.uuid" />
</template>
</component>
<!-- <ref-line v-if="dSelectWidgets.length === 0" /> -->
<!-- <size-control v-if="dSelectWidgets.length === 0" /> -->
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, defineProps, onMounted, ref } from 'vue'
import { mapGetters, mapActions, useStore } from 'vuex'
import { getTarget } from '@/common/methods/target'
import setWidgetData from '@/common/methods/DesignFeatures/setWidgetData'
import PointImg from '@/utils/plugins/pointImg'
import getComponentsData from '@/common/methods/DesignFeatures/setComponents'
import { debounce } from 'throttle-debounce'
import { move, moveInit } from '@/mixins/move'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
//
type TProps = {
pageDesignCanvasId: string
}
type TParentData = {
width?: number
height?: number
}
type TSetting = {
width?: number
height?: number
top?: number
left?: number
}
const store = useStore()
const { pageDesignCanvasId } = defineProps<TProps>()
const {
dPaddingTop, dPage, dZoom, dScreen, dWidgets,
dActiveElement, dSelectWidgets, dAltDown, dDraging,
dHoverUuid, showRotatable
} = useSetupMapGetters(['dPaddingTop', 'dPage', 'dZoom', 'dScreen', 'dWidgets', 'dActiveElement', 'dHoverUuid', 'dSelectWidgets', 'dAltDown', 'dDraging', 'showRotatable'])
let _dropIn: string | null = ''
let _srcCache: string | null = ''
onMounted(() => {
getScreen()
const pageDesignEl = document.getElementById('page-design')
if (!pageDesignEl) return
pageDesignEl.addEventListener('mousedown', handleSelection, false)
pageDesignEl.addEventListener('mousemove', debounce(100, false, handleMouseMove), false)
})
// components: {lineGuides},
// mixins: [moveInit],
// ...mapActions(['updateScreen', 'selectWidget', 'deleteWidget', 'addWidget', 'addGroup']),
// getBackground(data) {
// if (data.startsWith('http')) return `url(${data})`
// if (data.startsWith('linear-gradient')) return data
// },
async function dropOver(e: MouseEvent) {
if (dActiveElement.value.editable || dActiveElement.value.lock) {
return false
}
e.preventDefault()
let { data, type } = store.getters.selectItem
if (type !== 'image') {
return
}
if (!e || !e.target) return
const eventTarget = e.target as HTMLElement
const target = await getTarget(eventTarget)
if (!target) return
const uuid = target.getAttribute('data-uuid')
store.dispatch('setDropOver', uuid)
const imgEl = target?.firstElementChild?.firstElementChild as HTMLImageElement
if (eventTarget.getAttribute('putIn')) {
_dropIn = uuid
const imgUrl = data.value.thumb || data.value.url
!_srcCache && (_srcCache = imgEl.src)
imgEl.src = imgUrl
} else {
_srcCache && (imgEl.src = _srcCache)
_srcCache = ''
_dropIn = ''
}
}
async function drop(e: MouseEvent) {
if (!dDraging.value) {
return
}
if (!e || !e.target) return
const eventTarget = e.target as HTMLElement
store.commit('setDraging', false)
const dropIn = _dropIn
_dropIn = ''
store.dispatch('setDropOver', '-1')
store.commit('setShowMoveable', false) //
let lost = eventTarget.className !== 'design-canvas' // className === 'design-canvas' , id: "page-design-canvas"
// e.stopPropagation()
e.preventDefault()
let { data: item, type } = JSON.parse(JSON.stringify(store.getters.selectItem))
//
store.commit('selectItem', {})
let setting: TSetting = {}
if (!type) {
return
}
//
setting = await setWidgetData(type, item, setting)
//
const canvasEl = document.getElementById('page-design-canvas')
if (!canvasEl) return
const lostX = e.x - canvasEl.getBoundingClientRect().left
const lostY = e.y - canvasEl.getBoundingClientRect().top
//
if (type === 'group') {
let parent: TParentData = {}
const componentItem = await getComponentsData(item)
// item = await getComponentsData(item)
componentItem.forEach((element) => {
if (element.type === 'w-group') {
parent.width = element.width
parent.height = element.height
}
})
const half = {
x: parent.width ? (parent.width * store.getters.dZoom) / 100 / 2 : 0,
y: parent.height ? (parent.height * store.getters.dZoom) / 100 / 2 : 0
}
componentItem.forEach((element) => {
element.left += (lost ? lostX - half.x : e.layerX - half.x) * (100 / store.getters.dZoom)
element.top += (lost ? lostY - half.y : e.layerY - half.y) * (100 / store.getters.dZoom)
})
store.dispatch('addGroup', componentItem)
// addGroup(item)
}
//
const half = {
x: setting.width ? (setting.width * store.getters.dZoom) / 100 / 2 : 0,
y: setting.height ? (setting.height * store.getters.dZoom) / 100 / 2 : 0
}
// const half = { x: (this.dDragInitData.offsetX * this.dZoom) / 100, y: (this.dDragInitData.offsetY * this.dZoom) / 100 }
setting.left = (lost ? lostX - half.x : e.layerX - half.x) * (100 / store.getters.dZoom)
setting.top = (lost ? lostY - half.y : e.layerY - half.y) * (100 / store.getters.dZoom)
if (lost && type === 'image') {
// svg
const target = await getTarget(eventTarget)
if (!target) return
const targetType = target.getAttribute('data-type')
const uuid = target.getAttribute('data-uuid')
if (targetType === 'w-mask') {
//
store.commit('setShowMoveable', true) //
const widget = dWidgets.value.find((item: {uuid: string}) => item.uuid === uuid)
widget.imgUrl = item.value.url
// if (e.target.className.baseVal) {
// !widget.imgs && (widget.imgs = {})
// widget.imgs[`${e.target.className.baseVal}`] = item.value.url
// }
} else {
if (dropIn) {
const widget = dWidgets.value.find((item: {uuid: string}) => item.uuid == dropIn)
widget.imgUrl = item.value.url
console.log('加入+', widget)
store.commit('setShowMoveable', true) //
} else {
store.dispatch('addWidget', setting) //
// addWidget(setting) //
}
}
} else if (type === 'bg') {
console.log('背景图片放置')
} else if (type !== 'group') {
console.log(setting)
store.dispatch('addWidget', setting) //
// addWidget(setting) //
}
//
// this.$store.commit('selectItem', {})
}
function getScreen() {
const pageDesignEl = document.getElementById('page-design')
if (!pageDesignEl) return
store.dispatch('updateScreen', {
width: pageDesignEl.offsetWidth,
height: pageDesignEl.offsetHeight,
})
// updateScreen({
// width: pageDesignEl.offsetWidth,
// height: pageDesignEl.offsetHeight,
// })
}
async function handleMouseMove(e: MouseEvent) {
if (!e || !e.target) return
const imageTarget = e.target as HTMLImageElement
const pImg = new PointImg(imageTarget)
const { rgba } = pImg.getColorXY(e.offsetX, e.offsetY)
if (rgba && rgba === 'rgba(0,0,0,0)') {
console.log('解析点位颜色: ', rgba)
let target = await getTarget(imageTarget)
if (!target) return
target.style.pointerEvents = 'none'
setTimeout(() => {
if (!target) return
target.style.pointerEvents = 'auto'
}, 300)
}
}
async function handleSelection(e: MouseEvent) {
if (e.which === 3) {
return
}
if (!e || !e.target) return
let target = await getTarget(e.target as HTMLElement)
if (!target) return
let type = target.getAttribute('data-type')
if (type) {
let uuid = target.getAttribute('data-uuid')
if (uuid !== '-1' && !dAltDown.value) {
let widget = dWidgets.value.find((item: {uuid: string}) => item.uuid === uuid)
if (widget.parent !== '-1' && widget.parent !== dActiveElement.value.uuid && widget.parent !== dActiveElement.value.parent) {
uuid = widget.parent
}
}
//
// this.$store.commit('setMoveable', false)
if (showRotatable.value !== false) {
store.dispatch('selectWidget', {
uuid: uuid,
})
// selectWidget({
// uuid: uuid,
// })
}
if (uuid !== '-1') {
moveInit.methods.initmovement(e) // mixins
}
} else {
//
store.dispatch('selectWidget', {
uuid: '-1',
})
// selectWidget({
// uuid: '-1',
// })
}
}
function getlayers() {
return dWidgets.value.filter((item: { parent: string }) => item.parent === dPage.value.uuid)
}
function getChilds(uuid: string) {
return dWidgets.value.filter((item: { parent: string }) => item.parent === uuid)
}
// getIsActive(uuid) {
// if (this.dSelectWidgets.length > 0) {
// let widget = this.dSelectWidgets.find((item) => item.uuid === uuid)
// if (widget) {
// return true
// }
// return false
// } else {
// return uuid === this.dActiveElement.uuid
// }
// },
</script>
<style lang="less" scoped>
#page-design {
height: 100%;
// display: flex;
// align-items: center;
overflow: auto;
position: relative;
width: 100%;
.out-page {
margin: 0 auto;
padding: 60px;
position: relative;
.design-canvas {
// transition: all 0.3s;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
box-shadow: 1px 1px 10px 3px rgba(0, 0, 0, 0.1);
margin: 0 auto;
position: relative;
// z-index: -9999;
// overflow: hidden;
// overflow: auto;
}
// .design-canvas ::-webkit-scrollbar {
// display: none; /* Chrome Safari */
// }
}
}
</style>

View File

@ -9,107 +9,130 @@
<div></div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
<script lang="ts" setup>
import { watch, defineProps } from 'vue'
import { useStore } from 'vuex'
import Guides from '@scena/guides'
import Guides, { GuideOptions } from '@scena/guides'
export default defineComponent({
props: ['show'],
setup(props) {
const store = useStore()
const container = 'page-design' // page-design out-page
let guidesTop: any = null
let guidesLeft: any = null
type TProps = {
show: boolean
}
watch(
() => props.show,
(open) => {
open ? render() : destroy()
},
)
type TSameParams = {
backgroundColor: string,
lineColor: string
textColor: string
// direction: 'start',
// height: 30,
displayDragPos: boolean,
dragPosFormat: (v: string | number) => string,
}
watch(
() => store.getters.dZoom,
() => {
changeScroll()
},
)
type TGuidesData = Guides & GuideOptions
// onMounted(() => {
// // let scrollX = 0
// // let scrollY = 0
// // window.addEventListener('resize', () => {
// // guides.resize()
// // })
// // window.addEventListener('wheel', (e) => {
// // scrollX += e.deltaX
// // scrollY += e.deltaY
// // guides.scrollGuides(scrollY)
// // guides.scroll(scrollX)
// // })
// })
function destroy() {
guidesTop.destroy()
guidesLeft.destroy()
guidesTop = null
guidesLeft = null
}
function render() {
const sameParams: any = {
backgroundColor: '#f9f9fa',
lineColor: '#bec2c7',
textColor: '#999999',
// direction: 'start',
// height: 30,
displayDragPos: true,
dragPosFormat: (v: any) => v + 'px',
}
guidesTop = new Guides(document.getElementById(container), {
...sameParams,
type: 'horizontal',
className: 'my-horizontal',
}).on('changeGuides', (e) => {
console.log(e, e.guides)
// const el = document.getElementById('out-page')
// const top = 20 + (el?.offsetTop || 0)
// store.commit('updateGuidelines', { horizontalGuidelines: e.guides.map((x) => x + top) })
})
guidesLeft = new Guides(document.getElementById(container), {
...sameParams,
type: 'vertical',
className: 'my-vertical',
}).on('changeGuides', (e) => {
console.log(e, e.guides)
// store.commit('updateGuidelines', { verticalGuidelines: e.guides })
})
changeScroll()
}
function changeScroll() {
if (guidesTop && guidesLeft) {
const zoom = store.getters.dZoom / 100
guidesTop.zoom = zoom
guidesLeft.zoom = zoom
if (zoom < 0.9) {
guidesTop.unit = Math.floor(1 / zoom) * 50
guidesLeft.unit = Math.floor(1 / zoom) * 50
}
setTimeout(() => {
const el = document.getElementById('out-page')
const left = 60 + (el?.offsetLeft || 0)
const top = 30 + (el?.offsetTop || 0)
guidesTop.scroll(-left / zoom)
guidesTop.scrollGuides(-top / zoom)
guidesLeft.scroll(-top / zoom)
guidesLeft.scrollGuides(-(left - 30) / zoom)
}, 300)
}
}
},
const props = withDefaults(defineProps<TProps>(), {
show: false
})
const store = useStore()
const container = 'page-design' // page-design out-page
let guidesTop: TGuidesData | null = null
let guidesLeft: TGuidesData | null = null
watch(
() => props.show,
(open) => {
open ? render() : destroy()
},
)
watch(
() => store.getters.dZoom,
() => {
changeScroll()
},
)
// onMounted(() => {
// // let scrollX = 0
// // let scrollY = 0
// // window.addEventListener('resize', () => {
// // guides.resize()
// // })
// // window.addEventListener('wheel', (e) => {
// // scrollX += e.deltaX
// // scrollY += e.deltaY
// // guides.scrollGuides(scrollY)
// // guides.scroll(scrollX)
// // })
// })
function destroy() {
guidesTop?.destroy()
guidesLeft?.destroy()
guidesTop = null
guidesLeft = null
}
function render() {
const sameParams: TSameParams = {
backgroundColor: '#f9f9fa',
lineColor: '#bec2c7',
textColor: '#999999',
// direction: 'start',
// height: 30,
displayDragPos: true,
dragPosFormat: (v) => v + 'px',
}
const containerEl = document.getElementById(container)
if (!containerEl) return
guidesTop = new Guides(containerEl, {
...sameParams,
type: 'horizontal',
className: 'my-horizontal',
}).on('changeGuides', (e) => {
console.log(e, e.guides)
// const el = document.getElementById('out-page')
// const top = 20 + (el?.offsetTop || 0)
// store.commit('updateGuidelines', { horizontalGuidelines: e.guides.map((x) => x + top) })
})
guidesLeft = new Guides(containerEl, {
...sameParams,
type: 'vertical',
className: 'my-vertical',
}).on('changeGuides', (e) => {
console.log(e, e.guides)
// store.commit('updateGuidelines', { verticalGuidelines: e.guides })
})
changeScroll()
}
function changeScroll() {
if (guidesTop && guidesLeft) {
const zoom = store.getters.dZoom / 100
guidesTop.zoom = zoom
guidesLeft.zoom = zoom
if (zoom < 0.9) {
guidesTop.unit = Math.floor(1 / zoom) * 50
guidesLeft.unit = Math.floor(1 / zoom) * 50
}
setTimeout(() => {
if (guidesTop && guidesLeft) {
const el = document.getElementById('out-page')
const left = 60 + (el?.offsetLeft ?? 0)
const top = 30 + (el?.offsetTop ?? 0)
guidesTop.scroll(-left / zoom)
guidesTop.scrollGuides(-top / zoom)
guidesLeft.scroll(-top / zoom)
guidesLeft.scrollGuides(-(left - 30) / zoom)
}
}, 300)
}
}
</script>
<style lang="less">

View File

@ -1,10 +1,11 @@
<!--
* @Author: ShawnPhang
* @Date: 2021-08-03 17:50:21
* @Description: 旧大小控制组件已交由moveable控制
* @Description: 旧大小控制组件已交由moveable控制 (已不使用)
* @LastEditors: ShawnPhang
* @LastEditTime: 2021-08-09 11:13:09
-->
<template>
<div v-if="dActiveElement.record && dActiveElement.uuid !== '-1'" id="size-control">
<!-- 上左 -->
@ -99,73 +100,74 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
// import { mapGetters, mapActions } from 'vuex'
//
const NAME = 'size-control'
// //
// const NAME = 'size-control'
export default {
name: NAME,
data() {
return {
dirs: [],
}
},
computed: {
...mapGetters(['dActiveElement', 'dWidgets']),
left() {
return parseInt(this.dActiveElement.left)
},
top() {
return parseInt(this.dActiveElement.top)
},
width() {
return parseInt(this.dActiveElement.record.width)
},
height() {
return parseInt(this.dActiveElement.record.height)
},
},
watch: {},
methods: {
...mapActions(['dResize', 'initDResize', 'dResize', 'stopDResize']),
handlemousedown(e, dirs) {
e.stopPropagation()
this.dirs = dirs.split('-')
this.initDResize({
startX: e.pageX,
startY: e.pageY,
originX: this.dActiveElement.left,
originY: this.dActiveElement.top,
width: this.width,
height: this.height,
})
// export default {
// name: NAME,
// data() {
// return {
// dirs: [],
// }
// },
// computed: {
// ...mapGetters(['dActiveElement', 'dWidgets']),
// left() {
// return parseInt(this.dActiveElement.left)
// },
// top() {
// return parseInt(this.dActiveElement.top)
// },
// width() {
// return parseInt(this.dActiveElement.record.width)
// },
// height() {
// return parseInt(this.dActiveElement.record.height)
// },
// },
// watch: {},
// methods: {
// ...mapActions(['dResize', 'initDResize', 'dResize', 'stopDResize']),
// handlemousedown(e, dirs) {
// e.stopPropagation()
// this.dirs = dirs.split('-')
// this.initDResize({
// startX: e.pageX,
// startY: e.pageY,
// originX: this.dActiveElement.left,
// originY: this.dActiveElement.top,
// width: this.width,
// height: this.height,
// })
document.addEventListener('mousemove', this.handlemousemove, true)
document.addEventListener('mouseup', this.handlemouseup, true)
},
// document.addEventListener('mousemove', this.handlemousemove, true)
// document.addEventListener('mouseup', this.handlemouseup, true)
// },
handlemousemove(e) {
e.stopPropagation()
e.preventDefault()
// handlemousemove(e) {
// e.stopPropagation()
// e.preventDefault()
this.dResize({
x: e.pageX,
y: e.pageY,
dirs: this.dirs,
})
},
// this.dResize({
// x: e.pageX,
// y: e.pageY,
// dirs: this.dirs,
// })
// },
handlemouseup() {
document.removeEventListener('mousemove', this.handlemousemove, true)
document.removeEventListener('mouseup', this.handlemouseup, true)
this.stopDResize()
},
},
}
// handlemouseup() {
// document.removeEventListener('mousemove', this.handlemousemove, true)
// document.removeEventListener('mouseup', this.handlemouseup, true)
// this.stopDResize()
// },
// },
// }
</script>
<style lang="less" scoped>
/*
#size-control {
position: absolute;
.square {
@ -178,4 +180,5 @@ export default {
z-index: 999;
}
}
*/
</style>

View File

@ -1,396 +0,0 @@
<template>
<div id="zoom-control">
<ul v-show="show" class="zoom-selecter">
<li v-for="(item, index) in zoomList" :key="index" :class="['zoom-item', { 'zoom-item-active': activezoomIndex === index }]" @click.stop="selectItem(index)">
<!-- <i v-if="item.icon" :class="['iconfont', item.icon]"></i> -->
<span>{{ item.text }}</span>
<i v-if="activezoomIndex === index" class="iconfont icon-selected"></i>
</li>
</ul>
<div v-if="!hideControl" class="zoom-control-wrap">
<div :class="['zoom-icon radius-left', { disable: activezoomIndex === 0 }]" @click.stop="activezoomIndex > 0 ? sub() : ''">
<i class="iconfont icon-sub"></i>
</div>
<div :class="['zoom-text', { 'zoom-text-active': show }]" @click.stop="show = !show">{{ zoom.text }}</div>
<div :class="['zoom-icon radius-right', { disable: otherIndex === otherList.length - 1 }]" @click.stop="otherIndex < otherList.length - 1 ? add() : ''">
<i class="iconfont icon-add"></i>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import addMouseWheel from '@/common/methods/addMouseWheel'
//
const NAME = 'zoom-control'
let holder = null
export default {
name: NAME,
data() {
return {
hideControl: false,
activezoomIndex: 0,
zoomList: [
{
text: '25%',
value: 25,
},
{
text: '50%',
value: 50,
},
{
text: '75%',
value: 75,
},
{
text: '100%',
value: 100,
},
{
text: '125%',
value: 125,
},
{
text: '150%',
value: 150,
},
{
text: '200%',
value: 200,
},
{
text: '最佳尺寸',
value: -1,
// icon: 'icon-best-size',
},
],
show: false,
zoom: {
value: 0,
text: 0,
},
otherList: [
{
text: '250%',
value: 250,
},
{
text: '300%',
value: 300,
},
{
text: '350%',
value: 350,
},
{
text: '400%',
value: 400,
},
{
text: '450%',
value: 450,
},
{
text: '500%',
value: 500,
},
],
otherIndex: -1,
bestZoom: 0,
}
},
computed: {
...mapGetters(['dPage', 'dScreen', 'zoomScreenChange', 'dZoom']),
},
watch: {
activezoomIndex(value) {
if (value < 0 || value > this.zoomList.length - 1) {
return
}
this.zoom = JSON.parse(JSON.stringify(this.zoomList[value]))
},
otherIndex(value) {
if (value < 0 || value > this.otherList.length - 1) {
return
}
this.zoom = JSON.parse(JSON.stringify(this.otherList[value]))
},
zoom(value) {
let realValue = value.value
if (realValue === -1) {
realValue = this.calcZoom()
}
this.updateZoom(realValue)
this.autoFixTop()
},
dScreen: {
handler() {
this.screenChange()
},
deep: true,
},
zoomScreenChange() {
this.activezoomIndex = this.zoomList.length - 1
this.screenChange()
},
dPage: {
handler(val) {
this.screenChange()
},
deep: true,
},
},
async mounted() {
await this.$nextTick()
window.addEventListener('click', this.close)
if (this.$route.path === '/draw') {
this.activezoomIndex = 3
this.hideControl = true
} else {
this.activezoomIndex = this.zoomList.length - 1
}
//
addMouseWheel('page-design', (isDown) => {
this.mousewheelZoom(isDown)
})
//
window.addEventListener('resize', (event) => {
this.changeScreen()
})
},
beforeUnmount() {
window.removeEventListener('click', this.close)
},
methods: {
...mapActions(['updateZoom', 'updateScreen']),
changeScreen() {
clearTimeout(holder)
holder = setTimeout(() => {
const screen = document.getElementById('page-design')
this.updateScreen({
width: screen.offsetWidth,
height: screen.offsetHeight,
})
}, 300)
},
screenChange() {
//
if (this.activezoomIndex === this.zoomList.length - 1) {
this.updateZoom(this.calcZoom())
this.autoFixTop()
}
},
selectItem(index) {
this.activezoomIndex = index
this.otherIndex = -1
this.show = false
},
close(e) {
this.show = false
},
add() {
this.curAction = 'add'
this.show = false
if (this.activezoomIndex === this.zoomList.length - 2 || this.activezoomIndex === this.zoomList.length - 1) {
this.activezoomIndex = this.zoomList.length
// this.otherIndex += 1
if (this.bestZoom) {
this.nearZoom(true)
} else {
this.otherIndex += 1
}
return
}
if (this.activezoomIndex != this.zoomList.length) {
this.activezoomIndex++
return
}
if (this.otherIndex < this.otherList.length - 1) {
this.otherIndex++
}
},
sub() {
this.curAction = null
this.show = false
if (this.otherIndex === 0) {
this.otherIndex = -1
this.activezoomIndex = this.zoomList.length - 2
return
}
if (this.otherIndex != -1) {
this.otherIndex--
return
}
if (this.activezoomIndex === this.zoomList.length - 1) {
if (this.bestZoom) {
this.nearZoom()
} else {
this.activezoomIndex = this.zoomList.length - 2
}
return
}
if (this.activezoomIndex != 0) {
this.activezoomIndex--
}
},
mousewheelZoom(down) {
const value = Number(this.dZoom.toFixed(0))
if (down && value <= 1) return
this.updateZoom(down ? value - 1 : value + 1)
this.zoom.text = value + '%'
this.autoFixTop()
},
nearZoom(add) {
for (let i = 0; i < this.zoomList.length; i++) {
this.activezoomIndex = i
if (this.zoomList[i].value > this.bestZoom) {
if (add) break
} else if (this.zoomList[i].value < this.bestZoom) {
if (!add) break
}
}
this.bestZoom = 0
},
calcZoom() {
let widthZoom = ((this.dScreen.width - 142) * 100) / this.dPage.width
let heightZoom = ((this.dScreen.height - 122) * 100) / this.dPage.height
this.bestZoom = Math.min(widthZoom, heightZoom)
return this.bestZoom
},
async autoFixTop() {
await this.$nextTick()
const presetPadding = 60
const el = document.getElementById('out-page')
// const clientHeight = document.body.clientHeight - 54
const parentHeight = el.offsetParent.offsetHeight - 54
let padding = (parentHeight - el.offsetHeight) / 2
if (typeof this.curAction === 'undefined') {
padding += presetPadding / 2
}
this.curAction === 'add' && (padding -= presetPadding)
this.$store.commit('updatePaddingTop', padding > 0 ? padding : 0)
},
},
}
</script>
<style lang="less" scoped>
@color-select: #1b1634;
@color1: #ffffff; //
@color2: #ffffff; // Appears 3 times
@color3: #666666; //
@color4: #c2c2c2; //
@color5: rgba(0, 0, 0, 0.12); //
@z-border-color: #e6e6e6;
#zoom-control {
bottom: 20px;
position: absolute;
right: 302px;
z-index: 1000;
.zoom-control-wrap {
display: flex;
flex-direction: row;
font-size: 14px;
height: 40px;
.radius-left {
border-bottom-left-radius: 50%;
border-top-left-radius: 50%;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
}
.radius-right {
border-bottom-right-radius: 50%;
border-top-right-radius: 50%;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
}
.zoom-icon {
align-items: center;
background-color: @color2;
color: @color3;
cursor: pointer;
display: flex;
justify-content: center;
width: 40px;
&:hover {
background-color: @color1;
color: @color-select;
}
}
.disable {
color: @color4;
&:hover {
background-color: @color2;
color: @color4;
cursor: not-allowed;
}
}
.zoom-text {
user-select: none;
align-items: center;
background-color: @color2;
color: @color3;
cursor: pointer;
display: flex;
justify-content: center;
width: 60px;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
&:hover {
background-color: @color1;
color: @color-select;
}
}
}
.zoom-selecter {
background-color: @color1;
color: @color3;
position: absolute;
top: -8px;
transform: translateY(-100%);
width: 100%;
z-index: 1000;
&:after {
bottom: -8px;
content: '';
left: 50%;
position: absolute;
transform: translateX(-50%);
}
.zoom-item {
align-items: center;
cursor: pointer;
display: flex;
font-size: 14px;
height: 34px;
padding: 10px;
width: 100%;
i {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
span {
flex: 1;
}
&:hover {
background-color: @color5;
color: @color-select;
}
}
}
}
// #zoom-control-active {
// background-color: @color1;
// background-color: @color5;
// color: @color-select;
// color: @color-select;
// }
</style>

View File

@ -0,0 +1,69 @@
export type TZoomData = {
text: string
value: number
}
export const ZoomList: TZoomData[] = [
{
text: '25%',
value: 25,
},
{
text: '50%',
value: 50,
},
{
text: '75%',
value: 75,
},
{
text: '100%',
value: 100,
},
{
text: '125%',
value: 125,
},
{
text: '150%',
value: 150,
},
{
text: '200%',
value: 200,
},
{
text: '最佳尺寸',
value: -1,
// icon: 'icon-best-size',
},
]
export const OtherList: TZoomData[] = [
{
text: '250%',
value: 250,
},
{
text: '300%',
value: 300,
},
{
text: '350%',
value: 350,
},
{
text: '400%',
value: 400,
},
{
text: '450%',
value: 450,
},
{
text: '500%',
value: 500,
},
]

View File

@ -0,0 +1,382 @@
<template>
<div id="zoom-control">
<ul v-show="show" class="zoom-selecter">
<li v-for="(item, index) in zoomList" :key="index" :class="['zoom-item', { 'zoom-item-active': activezoomIndex === index }]" @click.stop="selectItem(index)">
<!-- <i v-if="item.icon" :class="['iconfont', item.icon]"></i> -->
<span>{{ item.text }}</span>
<i v-if="activezoomIndex === index" class="iconfont icon-selected"></i>
</li>
</ul>
<div v-if="!hideControl" class="zoom-control-wrap">
<div :class="['zoom-icon radius-left', { disable: activezoomIndex === 0 }]" @click.stop="activezoomIndex > 0 ? sub() : ''">
<i class="iconfont icon-sub"></i>
</div>
<div :class="['zoom-text', { 'zoom-text-active': show }]" @click.stop="show = !show">{{ zoom.text }}</div>
<div :class="['zoom-icon radius-right', { disable: otherIndex === otherList.length - 1 }]" @click.stop="otherIndex < otherList.length - 1 ? add() : ''">
<i class="iconfont icon-add"></i>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useStore } from 'vuex'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import addMouseWheel from '@/common/methods/addMouseWheel'
import { OtherList, TZoomData, ZoomList } from './data';
import { useSetupMapGetters } from '@/common/hooks/mapGetters';
import { useRoute } from 'vue-router';
const route = useRoute()
const store = useStore()
//
let holder: number | undefined
const hideControl = ref(false)
const activezoomIndex = ref(0)
const zoomList = ref<TZoomData[]>(ZoomList)
const show = ref(false)
const zoom = ref<TZoomData>({
value: 0,
text: '',
})
const otherList = ref<TZoomData[]>(OtherList)
const otherIndex = ref(-1)
const bestZoom = ref(0)
const curAction = ref('')
const { dPage, dScreen, zoomScreenChange, dZoom } = useSetupMapGetters(['dPage', 'dScreen', 'zoomScreenChange', 'dZoom'])
watch(
activezoomIndex,
(data) => {
if (data < 0 || data > zoomList.value.length - 1) {
return
}
zoom.value = JSON.parse(JSON.stringify(zoomList.value[data]))
}
)
watch(
otherIndex,
(data) => {
if (data < 0 || data > otherList.value.length - 1) {
return
}
zoom.value = JSON.parse(JSON.stringify(otherList.value[data]))
}
)
watch(
zoom,
(data) => {
let realValue = data.value
if (realValue === -1) {
realValue = calcZoom()
}
store.dispatch('updateZoom', realValue)
// updateZoom(realValue)
autoFixTop()
}
)
watch(
dScreen,
() => {
screenChange()
},
{ deep: true, }
)
watch(
zoomScreenChange,
() => {
activezoomIndex.value = zoomList.value.length - 1
screenChange()
}
)
watch(
dPage,
() => {
screenChange()
},
{ deep: true }
)
onMounted(async () => {
await nextTick()
window.addEventListener('click', close)
if (route.path === '/draw') {
activezoomIndex.value = 3
hideControl.value = true
} else {
activezoomIndex.value = zoomList.value.length - 1
}
//
addMouseWheel('page-design', (isDown: boolean) => {
mousewheelZoom(isDown)
})
//
window.addEventListener('resize', (event) => {
changeScreen()
})
})
onBeforeUnmount(() => {
window.removeEventListener('click', close)
})
// ...mapActions(['updateZoom', 'updateScreen']),
function changeScreen() {
clearTimeout(holder)
holder = setTimeout(() => {
const screen = document.getElementById('page-design')
if (!screen) return
store.dispatch('updateScreen', {
width: screen.offsetWidth,
height: screen.offsetHeight,
})
// updateScreen({
// width: screen.offsetWidth,
// height: screen.offsetHeight,
// })
}, 300)
}
function screenChange() {
//
if (activezoomIndex.value === zoomList.value.length - 1) {
store.dispatch('updateZoom', calcZoom())
// this.updateZoom(this.calcZoom())
autoFixTop()
}
}
function selectItem(index: number) {
activezoomIndex.value = index
otherIndex.value = -1
show.value = false
}
function close(_: MouseEvent) {
show.value = false
}
function add() {
curAction.value = 'add'
show.value = false
if (
activezoomIndex.value === zoomList.value.length - 2 ||
activezoomIndex.value === zoomList.value.length - 1
) {
activezoomIndex.value = zoomList.value.length
// this.otherIndex += 1
if (bestZoom.value) {
nearZoom(true)
} else {
otherIndex.value += 1
}
return
}
if (activezoomIndex.value != zoomList.value.length) {
activezoomIndex.value++
return
}
if (otherIndex.value < otherList.value.length - 1) {
otherIndex.value++
}
}
function sub() {
curAction.value = ''
show.value = false
if (otherIndex.value === 0) {
otherIndex.value = -1
activezoomIndex.value = zoomList.value.length - 2
return
}
if (otherIndex.value != -1) {
otherIndex.value--
return
}
if (activezoomIndex.value === zoomList.value.length - 1) {
if (bestZoom) {
nearZoom()
} else {
activezoomIndex.value = zoomList.value.length - 2
}
return
}
if (activezoomIndex.value != 0) {
activezoomIndex.value--
}
}
function mousewheelZoom(down: boolean) {
const value = Number(dZoom.value.toFixed(0))
if (down && value <= 1) return
store.dispatch('updateZoom', down ? value - 1 : value + 1)
// updateZoom(down ? value - 1 : value + 1)
zoom.value.text = (value + '%') as any
autoFixTop()
}
function nearZoom(add?: boolean) {
for (let i = 0; i < zoomList.value.length; i++) {
activezoomIndex.value = i
if (zoomList.value[i].value > bestZoom.value) {
if (add) break
} else if (zoomList.value[i].value < bestZoom.value) {
if (!add) break
}
}
bestZoom.value = 0
}
function calcZoom() {
let widthZoom = ((dScreen.value.width - 142) * 100) / dPage.value.width
let heightZoom = ((dScreen.value.height - 122) * 100) / dPage.value.height
bestZoom.value = Math.min(widthZoom, heightZoom)
return bestZoom.value
}
async function autoFixTop() {
await nextTick()
const presetPadding = 60
const el = document.getElementById('out-page')
if (!el) return
// const clientHeight = document.body.clientHeight - 54
const parentHeight = (el.offsetParent as HTMLElement).offsetHeight - 54
let padding = (parentHeight - el.offsetHeight) / 2
if (typeof curAction.value === 'undefined') {
padding += presetPadding / 2
}
curAction.value === 'add' && (padding -= presetPadding)
store.commit('updatePaddingTop', padding > 0 ? padding : 0)
}
defineExpose({
screenChange
})
</script>
<style lang="less" scoped>
@color-select: #1b1634;
@color1: #ffffff; //
@color2: #ffffff; // Appears 3 times
@color3: #666666; //
@color4: #c2c2c2; //
@color5: rgba(0, 0, 0, 0.12); //
@z-border-color: #e6e6e6;
#zoom-control {
bottom: 20px;
position: absolute;
right: 302px;
z-index: 1000;
.zoom-control-wrap {
display: flex;
flex-direction: row;
font-size: 14px;
height: 40px;
.radius-left {
border-bottom-left-radius: 50%;
border-top-left-radius: 50%;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
}
.radius-right {
border-bottom-right-radius: 50%;
border-top-right-radius: 50%;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
}
.zoom-icon {
align-items: center;
background-color: @color2;
color: @color3;
cursor: pointer;
display: flex;
justify-content: center;
width: 40px;
&:hover {
background-color: @color1;
color: @color-select;
}
}
.disable {
color: @color4;
&:hover {
background-color: @color2;
color: @color4;
cursor: not-allowed;
}
}
.zoom-text {
user-select: none;
align-items: center;
background-color: @color2;
color: @color3;
cursor: pointer;
display: flex;
justify-content: center;
width: 60px;
border-block-end: 1px solid @z-border-color;
border-block-start: 1px solid @z-border-color;
&:hover {
background-color: @color1;
color: @color-select;
}
}
}
.zoom-selecter {
background-color: @color1;
color: @color3;
position: absolute;
top: -8px;
transform: translateY(-100%);
width: 100%;
z-index: 1000;
&:after {
bottom: -8px;
content: '';
left: 50%;
position: absolute;
transform: translateX(-50%);
}
.zoom-item {
align-items: center;
cursor: pointer;
display: flex;
font-size: 14px;
height: 34px;
padding: 10px;
width: 100%;
i {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
span {
flex: 1;
}
&:hover {
background-color: @color5;
color: @color-select;
}
}
}
}
// #zoom-control-active {
// background-color: @color1;
// background-color: @color5;
// color: @color-select;
// color: @color-select;
// }
</style>

View File

@ -6,8 +6,8 @@
</div>
<div v-show="activeTab === 0" class="style-wrap">
<div v-show="showGroupCombined" style="padding: 2rem 0">
<el-button plain type="primary" class="gounp__btn" @click="realCombined">成组</el-button>
<icon-item-select label="" :data="alignIconList" @finish="alignAction" />
<el-button plain type="primary" class="gounp__btn" @click="handleCombine">成组</el-button>
<icon-item-select label="" :data="iconList" @finish="alignAction" />
</div>
<component :is="dActiveElement.type + '-style'" v-show="!showGroupCombined" v-if="dActiveElement.type" />
</div>
@ -17,58 +17,64 @@
</div>
</template>
<script>
<script setup lang="ts">
//
const NAME = 'style-panel'
import { mapGetters, mapActions } from 'vuex'
import alignIconList from '@/assets/data/AlignListData'
// const NAME = 'style-panel'
import { useStore } from 'vuex'
import alignIconList, { AlignListData } from '@/assets/data/AlignListData'
import iconItemSelect from '../settings/iconItemSelect.vue'
import { ref, watch } from 'vue';
import { useSetupMapGetters } from '@/common/hooks/mapGetters';
export default {
name: NAME,
components: { iconItemSelect },
data() {
return {
activeTab: 0,
alignIconList,
showGroupCombined: false,
}
},
computed: {
...mapGetters(['dActiveElement', 'dWidgets', 'dSelectWidgets']),
},
watch: {
dSelectWidgets: {
handler(items) {
setTimeout(() => {
this.showGroupCombined = items.length > 1
}, 100)
},
deep: true,
},
},
methods: {
...mapActions(['selectWidget', 'updateAlign', 'updateHoverUuid', 'getCombined', 'realCombined', 'ungroup', 'pushHistory']),
alignAction(item) {
const sWidgets = JSON.parse(JSON.stringify(this.dSelectWidgets))
this.getCombined().then((group) => {
for (let i = 0; i < sWidgets.length; i++) {
const element = sWidgets[i]
this.updateAlign({
align: item.value,
uuid: element.uuid,
group,
})
}
this.pushHistory()
})
},
layerChange(newLayer) {
this.$store.commit('setDWidgets', newLayer.reverse())
this.$store.commit('setShowMoveable', false)
},
const store = useStore();
const activeTab = ref(0)
const iconList = ref<AlignListData[]>(alignIconList)
const showGroupCombined = ref(false)
const { dActiveElement, dWidgets, dSelectWidgets } = useSetupMapGetters(['dActiveElement', 'dWidgets', 'dSelectWidgets'])
watch(
dSelectWidgets,
(items) => {
setTimeout(() => {
showGroupCombined.value = items.length > 1
}, 100)
},
{
deep: true
}
)
function handleCombine() {
store.dispatch('realCombined')
}
// ...mapActions(['selectWidget', 'updateAlign', 'updateHoverUuid', 'getCombined', 'realCombined', 'ungroup', 'pushHistory']),
function alignAction(item: AlignListData) {
const sWidgets = JSON.parse(JSON.stringify(dSelectWidgets.value))
store.dispatch('getCombined').then((group) => {
sWidgets.forEach((element: Record<string, any>) => {
store.dispatch('updateAlign', {
align: item.value,
uuid: element.uuid,
group,
})
// updateAlign({
// align: item.value,
// uuid: element.uuid,
// group,
// })
});
store.dispatch('pushHistory')
// pushHistory()
})
}
function layerChange(newLayer: Record<string, any>[]) {
store.commit('setDWidgets', newLayer.toReversed())
store.commit('setShowMoveable', false)
}
</script>
<style lang="less" scoped>

View File

@ -0,0 +1,24 @@
type TCommonImgListData = {
isDelect: boolean
cover: string
fail: boolean
top: number
left: number
width: number
height: number
title: string
}
type TCommonPhotoListData = {
listWidth: number
gap: number
thumb?: string
url: string
color: string
isDelect: boolean
width: number
height: number
model: string
}

View File

@ -2,87 +2,81 @@
<div id="widget-panel">
<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)">
<li v-for="(item, index) in state.widgetClassifyList" :key="index" :class="['classify-item', { 'active-classify-item': state.activeWidgetClassify === index }]" @click="clickClassify(index)">
<div class="icon-box"><i :class="['iconfont', 'icon', item.icon]" :style="item.style" /></div>
<p>{{ item.name }}</p>
</li>
</ul>
<a href="https://github.com/palxiao/poster-design" target="_blank" class="github"><img src="https://fe-doc.palxp.cn/images/github.svg" alt="Github" title="Github" /> 源码</a>
</div>
<div v-show="active" class="widget-wrap">
<div v-show="state.active" class="widget-wrap">
<keep-alive>
<component :is="widgetClassifyList[activeWidgetClassify].component" />
<component :is="state.widgetClassifyList[state.activeWidgetClassify].component" />
</keep-alive>
</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">
<div v-show="state.active" class="side-wrap">
<!-- <el-tooltip effect="dark" content="收起侧边栏" placement="right"> -->
<div class="pack__up" @click="active = false"></div>
<div class="pack__up" @click="state.active = false"></div>
<!-- </el-tooltip> -->
</div>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
//
const NAME = 'widget-panel'
import widgetClassifyListData from '@/assets/data/WidgetClassifyList.ts'
import { reactive, toRefs, onMounted, watch, nextTick, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { mapActions } from 'vuex'
// const NAME = 'widget-panel'
import widgetClassifyListData from '@/assets/data/WidgetClassifyList'
import { reactive, onMounted, watch, nextTick, } from 'vue'
// import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
export default {
name: NAME,
setup() {
// const store = useStore()
const route = useRoute()
const state = reactive({
widgetClassifyList: widgetClassifyListData,
activeWidgetClassify: 0,
active: true,
})
const clickClassify = (index: number) => {
state.activeWidgetClassify = index
state.active = true
}
onMounted(async () => {
await nextTick()
const { koutu } = route.query
koutu && (state.activeWidgetClassify = 4)
})
watch(
() => state.activeWidgetClassify,
(index) => {
if (index >= 0 && index < state.widgetClassifyList.length) {
state.widgetClassifyList[index].show = true
}
},
)
// const { proxy } = getCurrentInstance() as ComponentInternalInstance
// watch(
// () => state.active,
// () => {
// let screen = document.getElementById('page-design')
// nextTick(() => {
// proxy?.updateScreen({
// width: screen.offsetWidth,
// height: screen.offsetHeight,
// })
// })
// },
// )
return {
clickClassify,
...toRefs(state),
}
},
methods: {
...mapActions(['updateScreen']),
},
// const store = useStore()
const route = useRoute()
const state = reactive({
widgetClassifyList: widgetClassifyListData,
activeWidgetClassify: 0,
active: true,
})
const clickClassify = (index: number) => {
console.log('index' ,index)
state.activeWidgetClassify = index
state.active = true
}
onMounted(async () => {
await nextTick()
const { koutu } = route.query
koutu && (state.activeWidgetClassify = 4)
})
watch(
() => state.activeWidgetClassify,
(index) => {
if (index >= 0 && index < state.widgetClassifyList.length) {
state.widgetClassifyList[index].show = true
}
},
)
// const { proxy } = getCurrentInstance() as ComponentInternalInstance
// watch(
// () => state.active,
// () => {
// let screen = document.getElementById('page-design')
// nextTick(() => {
// proxy?.updateScreen({
// width: screen.offsetWidth,
// height: screen.offsetHeight,
// })
// })
// },
// )
defineExpose({
clickClassify
})
// ...mapActions(['updateScreen']),
</script>
<style lang="less" scoped>

View File

@ -8,132 +8,166 @@
<template>
<div class="wrap">
<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 v-for="c in state.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">
<ul v-if="state.showList" v-infinite-scroll="loadData" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<div class="list" :style="modelStyle.list">
<imageTip v-for="(item, i) in bgList" :key="i + 'i'" :detail="item">
<imageTip v-for="(item, i) in state.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>
<div v-show="state.loading" class="loading"><i class="el-icon-loading"></i> 拼命加载中</div>
<div v-show="state.loadDone" class="loading">全部加载完毕</div>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed } from 'vue'
<script lang="ts" setup>
import { reactive, computed, defineProps, defineExpose } from 'vue'
import api from '@/api'
import { mapActions, useStore } from 'vuex'
import { useStore } from 'vuex'
import { ElImage } from 'element-plus'
import { TGetImageListResult } from '@/api/material';
export default defineComponent({
props: {
model: {
default: 'widgetPanel'
}
},
setup(props) {
const store = useStore()
const state = reactive({
loading: false,
loadDone: false,
bgList: [],
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])
type TCommonPanelData = {
color: string
list: string
}
const pageOptions = { page: 0, pageSize: 20 }
type TTmpModalData = {
widgetPanel: TCommonPanelData
stylePanel: TCommonPanelData
}
const loadData = () => {
if (state.loading) {
return
}
load()
}
type TProps = {
model: 'widgetPanel'
}
const load = async (init: boolean = false) => {
if (state.loadDone) {
return
}
state.loading = true
pageOptions.page += 1
type TState = {
loading: boolean
loadDone: boolean
bgList: TGetImageListResult[]
showList: boolean
colors: string[]
if (init) {
state.bgList = []
pageOptions.page = 1
}
}
await api.material.getImagesList({ cate: 16, page: pageOptions.page }).then(({ list }: any) => {
if (list.length > 0) {
state.bgList.push(...list)
} else {
state.loadDone = true
}
})
const { model } = defineProps<TProps>()
setTimeout(() => {
state.loading = false
}, 100)
}
function setBGcolor(color: string) {
store.dispatch('updatePageData', {
key: 'backgroundImage',
value: '',
})
store.dispatch('updatePageData', {
key: 'backgroundColor',
value: color,
pushHistory: true,
})
store.dispatch('selectWidget', {
uuid: '-1',
})
}
return {
...toRefs(state),
load,
setBGcolor,
loadData,
modelStyle
}
},
methods: {
...mapActions(['selectWidget', 'updatePageData']),
async selectItem(item: any) {
// this.$store.commit('setShowMoveable', false) //
this.updatePageData({
key: 'backgroundTransform',
value: {},
})
this.updatePageData({
key: 'backgroundImage',
value: item.url,
pushHistory: true,
})
this.selectWidget({
uuid: '-1',
})
},
dragStart(e: any, item: any) {
this.$store.commit('selectItem', { data: {}, type: 'bg' })
},
},
const store = useStore()
const state = reactive<TState>({
loading: false,
loadDone: false,
bgList: [],
showList: true,
colors: ['#000000ff', '#999999ff', '#CCCCCCff', '#FFFFFFff', '#E65353ff', '#FFD835ff', '#70BC59ff', '#607AF4ff', '#976BEEff'],
})
//
const models: TTmpModalData = {
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[model])
const pageOptions = { page: 0, pageSize: 20 }
const loadData = () => {
if (state.loading) {
return
}
load()
}
const load = async (init: boolean = false) => {
if (state.loadDone) {
return
}
state.loading = true
pageOptions.page += 1
if (init) {
state.bgList = []
pageOptions.page = 1
}
await api.material.getImagesList({ cate: 16, page: pageOptions.page }).then(({ list }) => {
if (list.length > 0) {
state.bgList.push(...list)
} else {
state.loadDone = true
}
})
setTimeout(() => {
state.loading = false
}, 100)
}
function setBGcolor(color: string) {
store.dispatch('updatePageData', {
key: 'backgroundImage',
value: '',
})
store.dispatch('updatePageData', {
key: 'backgroundColor',
value: color,
pushHistory: true,
})
store.dispatch('selectWidget', {
uuid: '-1',
})
}
// ...mapActions(['selectWidget', 'updatePageData']),
async function selectItem(item: TGetImageListResult) {
// this.$store.commit('setShowMoveable', false) //
store.dispatch('updatePageData', {
key: 'backgroundTransform',
value: {},
})
store.dispatch('updatePageData', {
key: 'backgroundImage',
value: item.url,
pushHistory: true,
})
store.dispatch('selectWidget', {
uuid: '-1',
})
// this.updatePageData({
// key: 'backgroundTransform',
// value: {},
// })
// this.updatePageData({
// key: 'backgroundImage',
// value: item.url,
// pushHistory: true,
// })
// this.selectWidget({
// uuid: '-1',
// })
}
function dragStart(_: MouseEvent, _item: TGetImageListResult) {
store.commit('selectItem', { data: {}, type: 'bg' })
}
defineExpose({
load,
setBGcolor,
loadData,
modelStyle
})
</script>
<style lang="less" scoped>

View File

@ -212,9 +212,12 @@ export default defineComponent({
.list {
width: 100%;
padding: 3.1rem 0 0 1rem;
gap: 0px !important;
&__item {
overflow: hidden;
background: #f8fafc;
margin-bottom: 8px;
margin-right: 8px;
}
&__img {
cursor: grab;

View File

@ -2,137 +2,148 @@
* @Author: ShawnPhang
* @Date: 2021-08-27 15:16:07
* @Description: 模板列表
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-11-22 09:55:59
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<div class="wrap">
<search-header v-model="searchKeyword" @change="cateChange" />
<el-divider v-show="title" style="margin-top: 1.7rem" content-position="left">
<span style="font-weight: bold">{{ title }}</span>
<search-header v-model="state.searchKeyword" @change="cateChange" />
<el-divider v-show="state.title" style="margin-top: 1.7rem" content-position="left">
<span style="font-weight: bold">{{ state.title }}</span>
</el-divider>
<ul ref="listRef" v-infinite-scroll="load" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<img-water-fall :listData="list" @select="selectItem" />
<div v-show="loading" class="loading"><i class="el-icon-loading"></i> 拼命加载中</div>
<div v-show="loadDone" class="loading">全部加载完毕</div>
<img-water-fall :listData="state.list" @select="selectItem" />
<div v-show="state.loading" class="loading"><i class="el-icon-loading"></i> 拼命加载中</div>
<div v-show="state.loadDone" class="loading">全部加载完毕</div>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, ref } from 'vue'
<script lang="ts" setup>
import { reactive, ref, defineExpose } from 'vue'
import api from '@/api'
import { mapActions, mapGetters, useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
import { LocationQueryValue, useRoute, useRouter } from 'vue-router'
// import chooseType from './components/chooseType.vue'
// import editModel from './components/editModel.vue'
import searchHeader from './components/searchHeader.vue'
import useConfirm from '@/common/methods/confirm'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
import imgWaterFall from './components/imgWaterFall.vue'
import { IGetTempListData } from '@/api/home'
export default defineComponent({
components: { searchHeader },
setup() {
const listRef = ref(null)
const route = useRoute()
const store = useStore()
const state: any = reactive({
loading: false,
loadDone: false,
list: [],
title: '推荐模板',
searchKeyword: '',
})
const pageOptions: any = { page: 0, pageSize: 20, cate: 1 }
const { cate, edit } = route.query
cate && (pageOptions.cate = cate)
edit && store.commit('managerEdit', true)
type TState = {
loading: boolean
loadDone: boolean
list: IGetTempListData[]
title: string
searchKeyword: string
}
// onMounted(async () => {})
type TPageOptions = {
page: number,
pageSize: number,
cate: number | string
state?: string
}
const load = async (init: boolean = false, stat?: string) => {
stat && (pageOptions.state = stat)
const listRef = ref<HTMLElement | null>(null)
const route = useRoute()
const router = useRouter()
const store = useStore()
const state = reactive<TState>({
loading: false,
loadDone: false,
list: [],
title: '推荐模板',
searchKeyword: '',
})
if (init) {
listRef.value.scrollTop = 0
state.list = []
pageOptions.page = 0
state.loadDone = false
}
if (state.loadDone || state.loading) {
return
}
const { tempEditing, dHistoryParams } = useSetupMapGetters(['tempEditing', 'dHistoryParams'])
state.loading = true
pageOptions.page += 1
const pageOptions: TPageOptions = { page: 0, pageSize: 20, cate: 1 }
const { cate, edit } = route.query
cate && (pageOptions.cate = (cate as LocationQueryValue) || 1)
edit && store.commit('managerEdit', true)
const res = await api.home.getTempList({ search: state.searchKeyword, ...pageOptions })
res.list.length <= 0 && (state.loadDone = true)
state.list = state.list.concat(res.list)
// onMounted(async () => {})
setTimeout(() => {
state.loading = false
checkHeight()
}, 100)
const load = async (init: boolean = false, stat?: string) => {
stat && (pageOptions.state = stat)
if (init && listRef.value) {
listRef.value.scrollTop = 0
state.list = []
pageOptions.page = 0
state.loadDone = false
}
if (state.loadDone || state.loading) {
return
}
state.loading = true
pageOptions.page += 1
const res = await api.home.getTempList({ search: state.searchKeyword, ...pageOptions })
res.list.length <= 0 && (state.loadDone = true)
state.list = state.list.concat(res.list)
setTimeout(() => {
state.loading = false
checkHeight()
}, 100)
}
function cateChange(type: any) {
state.title = type.name
const init = pageOptions.cate != type.id
pageOptions.cate = type.id
load(init, pageOptions.state)
}
function checkHeight() {
if (!listRef.value) return
//
const isLess = listRef.value.offsetHeight > (listRef.value.firstElementChild as HTMLElement)?.offsetHeight
isLess && load()
}
// ...mapActions(['selectWidget', 'updatePageData', 'setTemplate', 'pushHistory']),
async function selectItem(item: IGetTempListData) {
store.commit('setShowMoveable', false) //
if (dHistoryParams.value.length > 0) {
const isPass = await useConfirm('提示', '使用模板后,当前页面将会被替换,是否继续', 'warning')
if (!isPass) {
return false
}
}
store.commit('managerEdit', false)
store.commit('setDWidgets', [])
function cateChange(type: any) {
state.title = type.name
const init = pageOptions.cate != type.id
pageOptions.cate = type.id
load(init, pageOptions.stat)
}
setTempId(item.id)
function checkHeight() {
//
const isLess = listRef.value.offsetHeight > listRef.value.firstElementChild.offsetHeight
isLess && load()
}
let result = null
if (!item.data) {
const res = await api.home.getTempDetail({ id: item.id })
result = JSON.parse(res.data)
} else {
result = JSON.parse(item.data)
}
const { page, widgets } = result
console.log(widgets)
return {
...toRefs(state),
load,
cateChange,
listRef,
}
},
computed: {
...mapGetters(['tempEditing', 'dHistoryParams']),
},
methods: {
...mapActions(['selectWidget', 'updatePageData', 'setTemplate', 'pushHistory']),
async selectItem(item: any) {
this.$store.commit('setShowMoveable', false) //
if (this.dHistoryParams.length > 0) {
const isPass = await useConfirm('提示', '使用模板后,当前页面将会被替换,是否继续', 'warning')
if (!isPass) {
return false
}
}
this.$store.commit('managerEdit', false)
this.$store.commit('setDWidgets', [])
this.setTempId(item.id)
let result = null
if (!item.data) {
const res = await api.home.getTempDetail({ id: item.id })
result = JSON.parse(res.data)
} else {
result = JSON.parse(item.data)
}
const { page, widgets } = result
console.log(widgets)
this.$store.commit('setDPage', page)
this.setTemplate(widgets)
setTimeout(() => {
this.$store.commit('zoomScreenChange')
}, 300)
this.selectWidget({
uuid: '-1',
})
},
store.commit('setDPage', page)
store.dispatch('setTemplate', widgets)
// setTemplate(widgets)
setTimeout(() => {
store.commit('zoomScreenChange')
}, 300)
store.dispatch('selectWidget', {
uuid: '-1'
})
// selectWidget({
// uuid: '-1',
// })
}
// action({ name, value }: any, item: any, index: number) {
// switch (name) {
// case 'edit':
@ -158,12 +169,17 @@ export default defineComponent({
// setTempStat({ id }: any, stat: string) {
// api.home.setTempStat({ id, stat })
// },
setTempId(tempId: number | string) {
const { id } = this.$route.query
this.$router.push({ path: '/home', query: { tempid: tempId, id }, replace: true })
},
},
function setTempId(tempId: number | string) {
const { id } = route.query
router.push({ path: '/home', query: { tempid: tempId, id }, replace: true })
}
defineExpose({
load,
cateChange,
listRef,
})
</script>
<style lang="less" scoped>

View File

@ -22,78 +22,74 @@
</div>
</template>
<script lang="ts">
const NAME = 'text-list-wrap'
<script lang="ts" setup>
// const NAME = 'text-list-wrap'
import wText from '../../widgets/wText/wText.vue'
import { mapActions, useStore } from 'vuex'
import { getCurrentInstance, ComponentInternalInstance } from 'vue'
import { useStore } from 'vuex'
export default {
name: NAME,
setup() {
const store: any = useStore()
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const selectBasicText = (item: any) => {
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wText.setting))
setting.text = '双击编辑文字' // item.text
setting.width = item.fontSize * setting.text.length
setting.fontSize = item.fontSize
setting.fontWeight = item.fontWeight
const { width: pW, height: pH } = store.getters.dPage
setting.left = pW / 2 - item.fontSize * 3
setting.top = pH / 2 - item.fontSize / 2
;(proxy as any).addWidget(setting)
}
const dragStart = (e: Element, item: any) => {
store.commit('setDraging', true)
store.commit('selectItem', { data: { value: item }, type: 'text' })
}
return {
selectBasicText,
dragStart,
}
},
data() {
return {
basicTextList: [
// {
// text: '',
// fontSize: 96,
// fontWeight: 'bold',
// },
{
text: '+ 添加文字',
fontSize: 60,
fontWeight: 'normal',
},
// {
// text: '+ ',
// fontSize: 40,
// fontWeight: 'normal',
// },
// {
// text: '',
// fontSize: 36,
// fontWeight: 'normal',
// },
// {
// text: '',
// fontSize: 28,
// fontWeight: 'normal',
// },
],
}
},
methods: {
...mapActions(['addWidget']),
},
type TBasicTextData = {
text: string
fontSize: number
fontWeight: string
}
const store = useStore()
const selectBasicText = (item: TBasicTextData) => {
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wText.setting))
setting.text = '双击编辑文字' // item.text
setting.width = item.fontSize * setting.text.length
setting.fontSize = item.fontSize
setting.fontWeight = item.fontWeight
const { width: pW, height: pH } = store.getters.dPage
setting.left = pW / 2 - item.fontSize * 3
setting.top = pH / 2 - item.fontSize / 2
store.dispatch('addWidget', setting)
// addWidget(setting)
}
const dragStart = (_: MouseEvent, item: any) => {
store.commit('setDraging', true)
store.commit('selectItem', { data: { value: item }, type: 'text' })
}
const basicTextList: TBasicTextData[] = [
// {
// text: '',
// fontSize: 96,
// fontWeight: 'bold',
// },
{
text: '+ 添加文字',
fontSize: 60,
fontWeight: 'normal',
},
// {
// text: '+ ',
// fontSize: 40,
// fontWeight: 'normal',
// },
// {
// text: '',
// fontSize: 36,
// fontWeight: 'normal',
// },
// {
// text: '',
// fontSize: 28,
// fontWeight: 'normal',
// },
]
defineExpose({
selectBasicText,
dragStart,
})
// ...mapActions(['addWidget'])
</script>
<style lang="less" scoped>

View File

@ -17,58 +17,59 @@
<i class="icon sd-AI_zhineng" />
<div class="text"><span>智能抠图</span> <span class="desc">上传图像一键去除背景</span></div>
</div>
<imageCutout ref="imageCutout" />
<imageCutout ref="imageCutoutRef" />
</div>
</template>
<script>
<script lang="ts" setup>
//
const NAME = 'tool-list-wrap'
// const NAME = 'tool-list-wrap'
// import api from '@/api'
import { mapActions, mapGetters } from 'vuex'
import { ref, onMounted } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import wQrcode from '../../widgets/wQrcode/wQrcode.vue'
import imageCutout from '@/components/business/image-cutout'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
export default {
name: NAME,
components: { imageCutout },
data() {
return {
loadDone: false,
}
},
computed: {
...mapGetters(['dPage']),
},
const store = useStore()
const route = useRoute()
mounted() {
// this.getDataList()
setTimeout(() => {
const { koutu } = this.$route.query
koutu && this.openImageCutout()
}, 300)
},
methods: {
...mapActions(['addWidget']),
async getDataList() {
if (this.loadDone || this.loading) {
return
}
this.loading = true
this.page += 1
},
addQrcode() {
this.$store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wQrcode.setting))
const { width: pW, height: pH } = this.dPage
setting.left = pW / 2 - setting.width / 2
setting.top = pH / 2 - setting.height / 2
this.addWidget(setting)
},
openImageCutout() {
this.$refs.imageCutout.open()
},
},
const loadDone = ref(false)
const imageCutoutRef = ref<typeof imageCutout | null>(null)
const { dPage } = useSetupMapGetters(['dPage'])
onMounted(() => {
// this.getDataList()
setTimeout(() => {
const { koutu } = route.query
koutu && openImageCutout()
}, 300)
})
// ...mapActions(['addWidget'])
// async function getDataList() {
// if (loadDone || loading) {
// return
// }
// loading = true
// page += 1
// }
function addQrcode() {
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wQrcode.setting))
const { width: pW, height: pH } = dPage.value
setting.left = pW / 2 - setting.width / 2
setting.top = pH / 2 - setting.height / 2
store.dispatch('addWidget', setting)
// addWidget(setting)
}
function openImageCutout() {
if (!imageCutoutRef.value) return
imageCutoutRef.value.open()
}
</script>

View File

@ -7,206 +7,247 @@
-->
<template>
<div class="wrap">
<el-tabs v-model="tabActiveName" :stretch="true" class="tabs" @tab-change="tabChange">
<el-tabs v-model="state.tabActiveName" :stretch="true" class="tabs" @tab-change="tabChange">
<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'">
<uploader v-model="percent" class="upload" @done="uploadDone">
<div v-show="state.tabActiveName === 'pics'">
<uploader v-model="state.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>
<div style="margin: 1rem; height: 100vh">
<photo-list ref="imgListRef" :edit="editOptions.photo" :isDone="isDone" :listData="imgList" @load="load" @drag="dragStart" @select="selectImg" />
<photo-list
ref="imgListRef"
:edit="state.editOptions.photo" :isDone="state.isDone"
:listData="state.imgList"
@load="load" @drag="dragStart" @select="selectImg"
/>
</div>
</div>
<div v-show="tabActiveName === 'design'" class="wrap">
<div v-show="state.tabActiveName === 'design'" class="wrap">
<ul ref="listRef" v-infinite-scroll="loadDesign" class="infinite-list" :infinite-scroll-distance="150" style="overflow: auto">
<img-water-fall :edit="editOptions.works" :listData="designList" @select="selectDesign" />
<img-water-fall :edit="state.editOptions.works" :listData="state.designList" @select="selectDesign" />
<!-- <div v-show="loading" class="loading"><i class="el-icon-loading"></i>拼命加载中..</div> -->
<div v-show="isDone" class="loading">全部加载完毕</div>
<div v-show="state.isDone" class="loading">全部加载完毕</div>
</ul>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, watch, nextTick, ref, onMounted } from 'vue'
import { ElTabPane, ElTabs } from 'element-plus'
<script lang="ts" setup>
import { reactive, toRefs, watch, nextTick, ref, onMounted, defineProps, defineExpose } from 'vue'
import { ElTabPane, ElTabs, TabPaneName } from 'element-plus'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import uploader from '@/components/common/Uploader'
import api from '@/api'
import wImage from '../../widgets/wImage/wImage.vue'
import setImageData from '@/common/methods/DesignFeatures/setImage'
import setImageData, { TItem2DataParam } from '@/common/methods/DesignFeatures/setImage'
import useConfirm from '@/common/methods/confirm'
import { TGetImageListResult } from '@/api/material'
import photoList from './components/photoList.vue'
import imgWaterFall from './components/imgWaterFall.vue'
import { TUploadDoneData } from '@/components/common/Uploader/index.vue'
import { IGetTempListData } from '@/api/home'
export default defineComponent({
components: { uploader, ElTabPane, ElTabs },
props: ['active'],
setup(props) {
const router = useRouter()
const store = useStore()
const state: any = reactive({
prePath: 'user',
percent: { num: 0 }, //
imgList: [],
designList: [],
isDone: false,
editOptions: [],
listRef: null,
imgListRef: null,
tabActiveName: '',
})
let loading = false
let page = 0
let listPage = 0
type TProps = {
active?: number
}
const load = (init: boolean) => {
if (init) {
state.imgList = []
page = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
page += 1
api.material.getMyPhoto({ page }).then(({ list }: any) => {
list.length <= 0 ? (state.isDone = true) : (state.imgList = state.imgList.concat(list))
setTimeout(() => {
loading = false
checkHeight(state.imgListRef.getRef(), load)
}, 100)
})
}
const loadDesign = (init: boolean = false) => {
if (init) {
state.designList = []
listPage = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
listPage += 1
api.home.getMyDesign({ page: listPage, pageSize: 10 }).then(({ list }: any) => {
list.length <= 0
? (state.isDone = true)
: (state.designList = state.designList.concat(
list.map((x: any) => {
x.cover = x.cover + '?r=' + Math.random()
return x
}),
))
setTimeout(() => {
loading = false
checkHeight(state.listRef, loadDesign)
}, 100)
})
}
type TState = {
prePath: string,
percent: { num: number }, //
imgList: IGetTempListData[],
designList: IGetTempListData[],
isDone: boolean,
editOptions: Record<string, any>,
tabActiveName: string,
}
function checkHeight(el: any, loadFn: Function) {
//
const isLess = el.offsetHeight > el.firstElementChild.offsetHeight
isLess && loadFn()
}
const props = defineProps<TProps>()
onMounted(() => {
load(true)
nextTick(() => {
state.tabActiveName = 'pics'
})
})
const router = useRouter()
const store = useStore()
const listRef = ref<HTMLElement | null>(null)
const imgListRef = ref<typeof photoList | null>(null)
const selectImg = async (index: number) => {
const item: any = state.imgList[index]
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wImage.setting))
const img: any = await setImageData(item)
setting.width = img.width
setting.height = img.height // parseInt(100 / item.value.ratio, 10)
setting.imgUrl = item.url
const { width: pW, height: pH } = store.getters.dPage
setting.left = pW / 2 - img.width / 2
setting.top = pH / 2 - img.height / 2
store.dispatch('addWidget', setting)
}
const deleteImg = async ({ i, item }: any) => {
store.commit('setShowMoveable', false) //
const isPass = await useConfirm('警告', '删除后不可找回,已引用资源将会失效,请谨慎操作', 'warning')
if (!isPass) {
return false
}
const arr = item.url.split('/')
let key = arr.splice(3, arr.length - 1).join('/')
api.material.deleteMyPhoto({ id: item.id, key })
state.imgListRef.delItem(i) //
}
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' })
}
const uploadDone = async (res: any) => {
await api.material.addMyPhoto(res)
state.imgList = []
load(true)
}
const state = reactive<TState>({
prePath: 'user',
percent: { num: 0 }, //
imgList: [],
designList: [],
isDone: false,
editOptions: [],
tabActiveName: '',
})
const tabChange = (tabName: string) => {
if (tabName === 'design') {
loadDesign(true)
}
}
let loading = false
let page = 0
let listPage = 0
const selectDesign = async (item: any) => {
// const { id }: any = state.designList[index]
const { id }: any = item
window.open(`${window.location.protocol + '//' + window.location.host}/home?id=${id}`)
const load = (init?: boolean) => {
if (init) {
state.imgList = []
page = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
page += 1
api.material.getMyPhoto({ page }).then(({ list }) => {
if (list.length <= 0) {
state.isDone = true
} else {
state.imgList = state.imgList.concat(list)
}
setTimeout(() => {
loading = false
if (!imgListRef.value) return
checkHeight(imgListRef.value.getRef(), load)
}, 100)
})
}
const openPSD = () => {
window.open(router.resolve('/psd').href, '_blank')
}
const loadDesign = (init: boolean = false) => {
if (init) {
state.designList = []
listPage = 0
state.isDone = false
}
if (state.isDone || loading) {
return
}
loading = true
listPage += 1
api.home.getMyDesign({ page: listPage, pageSize: 10 }).then(({ list }) => {
list.length <= 0
? (state.isDone = true)
: (state.designList = state.designList.concat(
list.map((x) => {
x.cover = x.cover + '?r=' + Math.random()
return x
}),
))
setTimeout(() => {
loading = false
if (!listRef.value) return
checkHeight(listRef.value, loadDesign)
}, 100)
})
}
return {
...toRefs(state),
selectDesign,
loadDesign,
load,
uploadDone,
selectImg,
deleteImg,
dragStart,
tabChange,
openPSD,
}
},
function checkHeight(el: HTMLElement, loadFn: Function) {
//
if (el.offsetHeight && el.firstElementChild) {
const isLess = el.offsetHeight > (el.firstElementChild as HTMLElement).offsetHeight
isLess && loadFn()
}
}
onMounted(() => {
load(true)
nextTick(() => {
state.tabActiveName = 'pics'
})
})
const selectImg = async (index: number) => {
const item = state.imgList[index]
store.commit('setShowMoveable', false) //
let setting = JSON.parse(JSON.stringify(wImage.setting))
const img = await setImageData(item)
setting.width = img.width
setting.height = img.height // parseInt(100 / item.value.ratio, 10)
setting.imgUrl = item.url
const { width: pW, height: pH } = store.getters.dPage
setting.left = pW / 2 - img.width / 2
setting.top = pH / 2 - img.height / 2
store.dispatch('addWidget', setting)
}
type controlImgParam = {
i: number
item: Required<TItem2DataParam>
}
const deleteImg = async ({ i, item }: controlImgParam) => {
store.commit('setShowMoveable', false) //
const isPass = await useConfirm('警告', '删除后不可找回,已引用资源将会失效,请谨慎操作', 'warning')
if (!isPass) {
return false
}
const arr = item.url.split('/')
let key = arr.splice(3, arr.length - 1).join('/')
api.material.deleteMyPhoto({ id: item.id, key })
if (!imgListRef.value) return
imgListRef.value.delItem(i) //
}
const deleteWorks = async ({ i, item }: controlImgParam) => {
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' })
}
const uploadDone = async (res: TUploadDoneData) => {
await api.material.addMyPhoto(res)
state.imgList = []
load(true)
}
const tabChange = (tabName: TabPaneName) => {
if (tabName === 'design') {
loadDesign(true)
}
}
const selectDesign = async (item: IGetTempListData) => {
// const { id }: any = state.designList[index]
const { id } = item
window.open(`${window.location.protocol + '//' + window.location.host}/home?id=${id}`)
}
const openPSD = () => {
window.open(router.resolve('/psd').href, '_blank')
}
defineExpose({
selectDesign,
loadDesign,
load,
uploadDone,
selectImg,
deleteImg,
dragStart,
tabChange,
openPSD,
})
</script>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2023-10-04 02:04:04
* @Description: 列表分类头部
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 02:30:59
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<div v-if="!isBack" class="content__wrap">
@ -21,22 +21,34 @@
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { defineProps, defineEmits } 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 }
},
})
export type TClassHeaderTypeData = {
name: string
}
type TProps = {
types: TClassHeaderTypeData[]
isBack?: boolean
}
type TEmits = {
(event: 'select', data: string[]): void
(event: 'back'): void
}
const { types, isBack } = defineProps<TProps>()
const emit = defineEmits<TEmits>()
const select = (item: any) => {
emit('select', item)
}
const back = () => {
emit('back')
}
defineExpose({ select, back })
</script>
<style lang="less" scoped>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2023-10-04 19:12:40
* @Description: 图片描述ToolTip
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 22:51:06
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<el-tooltip :disabled="!detail.author" :offset="1" effect="light" placement="bottom-start" :hide-after="0" :enterable="false" raw-content>
@ -17,15 +17,18 @@
</el-tooltip>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import { defineProps } from 'vue'
export type TImageTipDetailData = {
author: string
description: string
}
type Tprops = {
detail: TImageTipDetailData
}
const { detail } = defineProps<Tprops>()
export default defineComponent({
props: {
detail: {},
},
setup() {
return {}
},
})
</script>

View File

@ -2,14 +2,18 @@
* @Author: ShawnPhang
* @Date: 2021-12-16 16:20:16
* @Description: 瀑布流组件
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-12-11 11:45:24
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<div ref="imgWaterFall" :style="{ height: countHeight + 'px' }" class="img-water-fall">
<div ref="imgWaterFall" :style="{ height: state.countHeight + 'px' }" class="img-water-fall">
<!-- backgroundImage: `url(${item.cover})` -->
<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 }">
<div
v-for="(item, i) in state.list" :key="i + 'iwf'"
:style="{ top: item.top + 'px', left: item.left + 'px', width: state.width + 'px', height: item.height + 'px' }"
class="img-box" @click.stop="selectItem(item, i)"
>
<edit-model v-if="edit" :options="props.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)" />
@ -20,76 +24,82 @@
</div>
</template>
<script>
const NAME = 'img-water-fall'
import { defineComponent, toRefs, reactive, watch } from 'vue'
<script lang="ts" setup>
// const NAME = 'img-water-fall'
import { IGetTempListData } from '@/api/home';
import { reactive, watch, defineProps, defineExpose, defineEmits } from 'vue'
export default defineComponent({
name: NAME,
props: {
listData: {
type: Array,
required: true,
},
edit: {}
type TProps = {
listData: IGetTempListData[]
edit?: Record<string, any>
}
type TState = {
width: number
countHeight: number
list: IGetTempListData[]
}
type TEmits = {
(event: 'select', data: IGetTempListData): void
(event: 'load'): void
}
const props = defineProps<TProps>()
const emit = defineEmits<TEmits>()
const state = reactive<TState>({
width: 146, //
list: [],
countHeight: 0,
})
const columnHeights: number[] = [] //
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
},
emits: ['select', 'load'],
setup(props, { emit }) {
const state = reactive({
width: 146, //
list: [],
countHeight: 0,
})
)
const columnHeights = [] //
const columnNums = 2 //
const gap = 7 //
const load = () => {
emit('load')
}
const selectItem = (value: IGetTempListData, index: number) => {
emit('select', value)
}
const loadError = (item: IGetTempListData) => {
item.fail = true
}
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,
}
},
defineExpose({
load,
selectItem,
loadError,
})
</script>

View File

@ -2,14 +2,23 @@
* @Author: ShawnPhang
* @Date: 2022-02-23 15:48:52
* @Description: 图片列表组件 Bookshelf Layout
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-02-29 16:52:37
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<ul ref="listRef" class="img-list-wrap" :style="{ paddingBottom: isShort ? '15px' : '200px' }" @scroll="scrollEvent($event)">
<ul ref="listRef" class="img-list-wrap" :style="{ paddingBottom: props.isShort ? '15px' : '200px' }" @scroll="scrollEvent($event)">
<div class="list">
<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-for="(item, i) in state.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="props.edit" :options="props.edit" :data="{ item, i }">
<div v-if="item.isDelect" class="list__mask">已删除</div>
<el-image class="img transparent-bg" :src="item.thumb || item.url" :style="{ height: getInnerHeight(item) + 'px' }" lazy loading="lazy" />
</edit-model>
@ -24,163 +33,184 @@
</template>
</div>
</div>
<div v-if="!isDone" v-show="loading" class="loading"><i class="el-icon-loading" /> 拼命加载中</div>
<div v-if="!props.isDone" v-show="state.loading" class="loading"><i class="el-icon-loading" /> 拼命加载中</div>
<div v-else class="loading">全部加载完毕</div>
</ul>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, watch, nextTick } from 'vue'
<script lang="ts" setup>
import { reactive, watch, nextTick, defineProps, defineExpose, defineEmits, ref } from 'vue'
import DragHelper from '@/common/hooks/dragHelper'
import setImageData from '@/common/methods/DesignFeatures/setImage'
import setImageData, { TItem2DataParam } from '@/common/methods/DesignFeatures/setImage'
import { IGetTempListData } from '@/api/home';
export default defineComponent({
props: {
listData: {},
edit: {},
isDone: {},
isShort: {
default: false,
},
},
emits: ['load', 'drag', 'select'],
setup(props, context) {
const state: any = reactive({
loading: true,
list: [],
listRef: null,
})
type TProps = {
listData: IGetTempListData[]
edit?: Record<string, any>
isDone?: boolean
isShort?: boolean
}
const dragHelper = new DragHelper()
let isDrag = false
let startPoint = { x: 99999, y: 99999 }
const mouseup = (e: any) => {
e.preventDefault()
setTimeout(() => {
isDrag = false
startPoint = { x: 99999, y: 99999 }
}, 10)
type TEmits = {
(event: 'load'): void
(event: 'select', data: number): void
(event: 'drag', data: number): void
}
type TState = {
loading: boolean
list: IGetTempListData[]
}
const props = withDefaults(defineProps<TProps>(), {
isShort: false,
listData: () => ([])
})
const emit = defineEmits<TEmits>()
const listRef = ref<HTMLElement | null>(null)
const state = reactive<TState>({
loading: true,
list: [],
})
const dragHelper = new DragHelper()
let isDrag = false
let startPoint = { x: 99999, y: 99999 }
const mouseup = (e: MouseEvent) => {
e.preventDefault()
setTimeout(() => {
isDrag = false
startPoint = { x: 99999, y: 99999 }
}, 10)
}
const mousemove = (e: MouseEvent) => {
e.preventDefault()
if (e.x - startPoint.x > 2 || e.y - startPoint.y > 2) {
isDrag = true
}
}
watch(
() => props.listData,
async (newList: IGetTempListData[], oldList: IGetTempListData[]) => {
!oldList && (oldList = [])
if (newList.length <= 0) {
state.list.length = 0
return
}
const mousemove = (e: any) => {
e.preventDefault()
if (e.x - startPoint.x > 2 || e.y - startPoint.y > 2) {
isDrag = true
}
}
watch(
() => props.listData,
async (newList: any, oldList: any) => {
!oldList && (oldList = [])
if (newList.length <= 0) {
state.list.length = 0
let list = newList.filter((v: IGetTempListData) => !newList.includes(v) || !oldList.includes(v)) // difference
list = JSON.parse(JSON.stringify(list))
const marginRight = 6 //
const limitWidth = (await getFatherWidth()) - marginRight
const standardHeight = 280 //
const neatArr: IGetTempListData[][] = [] //
function factory(cutArr: IGetTempListData[]) {
return new Promise<{ height: number, list: IGetTempListData[] }>((resolve) => {
const lineup = list.shift()
if (!lineup) {
resolve({ height: calculate(cutArr), list: cutArr })
return
}
let list = newList.filter((v: any) => !newList.includes(v) || !oldList.includes(v)) // difference
list = JSON.parse(JSON.stringify(list))
const marginRight = 6 //
const limitWidth = (await getFatherWidth()) - marginRight
const standardHeight = 280 //
const neatArr: any = [] //
function factory(cutArr: any) {
return new Promise((resolve) => {
const lineup = list.shift()
if (!lineup) {
resolve({ height: calculate(cutArr), list: cutArr })
return
}
cutArr.push(lineup)
const finalHeight = calculate(cutArr)
if (finalHeight > standardHeight) {
resolve(factory(cutArr))
} else {
resolve({ height: finalHeight, list: cutArr })
}
})
}
function calculate(cutArr: any) {
let cumulate = 0
for (const iterator of cutArr) {
const { width, height } = iterator
cumulate += width / height
}
return (limitWidth - marginRight * (cutArr.length - 1)) / cumulate
}
async function handleList() {
if (list.length <= 0) {
return
}
const { list: newList, height }: any = await factory([list.shift()])
neatArr.push(
newList.map((x: any, index) => {
x.listWidth = (x.width / x.height) * height
x.gap = index !== newList.length - 1 ? marginRight : 0
return x
}),
)
if (list.length > 0) {
await handleList()
}
cutArr.push(lineup)
const finalHeight = calculate(cutArr)
if (finalHeight > standardHeight) {
resolve(factory(cutArr))
} else {
resolve({ height: finalHeight, list: cutArr })
}
})
}
function calculate(cutArr: IGetTempListData[]) {
let cumulate = 0
for (const iterator of cutArr) {
const { width, height } = iterator
cumulate += width / height
}
return (limitWidth - marginRight * (cutArr.length - 1)) / cumulate
}
async function handleList() {
if (list.length <= 0) {
return
}
const { list: newList, height } = await factory([(list.shift() as IGetTempListData)])
neatArr.push(
newList.map((x: IGetTempListData, index: number) => {
x.listWidth = (x.width / x.height) * height
x.gap = index !== newList.length - 1 ? marginRight : 0
return x
}),
)
if (list.length > 0) {
await handleList()
state.list = state.list.concat(neatArr.flat(1))
state.loading = false
},
)
async function getFatherWidth() {
await nextTick()
const dom = state.listRef
const father = dom.parentElement || dom.parentNode
return father.offsetWidth
}
function getRef() {
// ref
return state.listRef
}
const load = () => {
state.loading = true
context.emit('load')
}
const select = (i: number) => {
!isDrag && !state.list[i].isDelect && context.emit('select', i)
}
const dragStart = async (e: Event | any, i: number) => {
e.preventDefault()
startPoint = { x: e.x, y: e.y }
if (!state.list[i].isDelect) {
const img = await setImageData(state.list[i])
dragHelper.start(e, img.canvasWidth)
context.emit('drag', i)
}
}
function delItem(i: number) {
state.list[i].isDelect = true
}
const scrollEvent = (e: any) => {
if (e.target.scrollTop + e.target.offsetHeight + 200 >= e.target.scrollHeight) {
load()
}
}
const getInnerHeight = ({ height, listWidth, width }: any) => (height * listWidth) / width
return {
load,
dragStart,
select,
...toRefs(state),
delItem,
scrollEvent,
getRef,
mouseup,
mousemove,
getInnerHeight,
}
await handleList()
state.list = state.list.concat(neatArr.flat(1))
state.loading = false
},
)
async function getFatherWidth() {
await nextTick()
if (!listRef.value) return 0
const father = listRef.value.parentElement ?? listRef.value.parentNode
if (!father) return 0
return (father as HTMLElement).offsetWidth
}
function getRef() {
// ref
return listRef
}
const load = () => {
state.loading = true
emit('load')
}
const select = (i: number) => {
!isDrag && !state.list[i].isDelect && emit('select', i)
}
const dragStart = async (e: Event | any, i: number) => {
e.preventDefault()
startPoint = { x: e.x, y: e.y }
if (!state.list[i].isDelect) {
const setImageParams: TItem2DataParam = {
width: state.list[i].width,
height: state.list[i].height,
url: state.list[i].url || '',
model: state.list[i].model
}
const img = await setImageData(setImageParams)
dragHelper.start(e, img.canvasWidth)
emit('drag', i)
}
}
function delItem(i: number) {
state.list[i].isDelect = true
}
const scrollEvent = (e: any) => {
if (e.target.scrollTop + e.target.offsetHeight + 200 >= e.target.scrollHeight) {
load()
}
}
const getInnerHeight = ({ height, listWidth, width }: any) => (height * listWidth) / width
defineExpose({
load,
dragStart,
select,
delItem,
scrollEvent,
getRef,
mouseup,
mousemove,
getInnerHeight,
})
</script>

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-01-27 11:05:48
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-04 01:53:10
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @Date: 2024-03-06 21:16:00
-->
<template>
<div class="search__wrap">
@ -11,66 +11,85 @@
<div class="search__type"><i class="iconfont icon-ego-caidan" /></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="type in materialCates" :key="type.id" @click="action('change', type, type.id)">
<span :class="['cate__text', { 'cate--select': +currentIndex === type.id }]">{{ type.name }}</span>
<el-dropdown-item
v-for="type in state.materialCates" :key="type.id"
@click="action('change', type, type.id)"
>
<span :class="['cate__text', { 'cate--select': + state.currentIndex === type.id }]">{{ type.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span v-else style="width: 1rem"></span>
<el-input v-model="searchValue" size="large" placeholder="输入关键词搜索" class="input-with-select">
<el-input v-model="state.searchValue" size="large" placeholder="输入关键词搜索" class="input-with-select">
<template #append>
<el-button><i class="iconfont icon-search"></i></el-button>
</template>
</el-input>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, watch } from 'vue'
<script lang="ts" setup>
import { reactive, toRefs, watch, defineProps, defineEmits, defineExpose } from 'vue'
import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
import { useRoute } from 'vue-router'
import api from '@/api'
export default defineComponent({
components: { ElDropdown, ElDropdownItem, ElDropdownMenu },
props: ['type', 'modelValue'],
emits: ['update:modelValue'],
setup(props, context) {
const route = useRoute()
const state: any = reactive({
searchValue: '',
materialCates: [],
currentIndex: 1,
})
type TProps = {
type?: string
modelValue?: string
}
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)
cate && action('change', state.materialCates[Number(cate)], Number(cate))
})
}
type TEmits = {
(event: 'update:modelValue', data: string): void
(event: 'change', data: TMaterialCatesData): void
}
watch(
() => state.searchValue,
() => {
context.emit('update:modelValue', state.searchValue)
},
)
type TMaterialCatesData = {id: string | number, name: string}
function action(fn: string, type: any, currentIndex: number | string) {
currentIndex && (state.currentIndex = currentIndex)
context.emit(fn, type)
}
return {
...toRefs(state),
action,
}
},
type TState = {
searchValue: string
materialCates: TMaterialCatesData[]
currentIndex: number | string
}
const props = defineProps<TProps>()
const emit = defineEmits<TEmits>()
const route = useRoute()
const state = reactive<TState>({
searchValue: '',
materialCates: [],
currentIndex: 1,
})
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 as string)
cate && action('change', state.materialCates[Number(cate)], Number(cate))
})
}
watch(
() => state.searchValue,
() => {
emit('update:modelValue', state.searchValue)
},
)
function action(fn: 'change', type: TMaterialCatesData, currentIndex: number | string) {
currentIndex && (state.currentIndex = currentIndex)
emit(fn, type)
}
defineExpose({
action
})
</script>
<style lang="less" scoped>

View File

@ -6,27 +6,27 @@
* @LastEditTime: 2023-06-29 17:53:39
-->
<template>
<el-card class="box-card" shadow="hover" :body-style="{ padding: effectSelect ? '20px' : 0 }">
<el-card class="box-card" shadow="hover" :body-style="{ padding: state.effectSelect ? '20px' : 0 }">
<template #header>
<div class="card-header">
<template v-if="effectSelect">
<component :is="effectSelect" class="demo" />
<template v-if="state.effectSelect">
<component :is="state.effectSelect" class="demo" />
</template>
<div v-show="!effectSelect"></div>
<div v-show="!state.effectSelect"></div>
<span class="title">图片容器</span>
<el-popover :visible="visiable" placement="bottom-end" :width="260" trigger="click">
<el-popover :visible="state.visiable" placement="bottom-end" :width="260" trigger="click">
<div class="box__header">
<el-radio-group v-model="type" size="small">
<el-radio-group v-model="state.type" size="small">
<el-radio-button label="160">形状</el-radio-button>
<el-radio-button label="166">框架</el-radio-button>
</el-radio-group>
</div>
<div class="select__box">
<div class="select__box__select-item" @click="select(null)"></div>
<el-image v-for="(item, i) in list" :key="i + 'l'" class="select__box__select-item" :src="item.thumbUrl" fit="contain" @click="select(item.imgUrl)"></el-image>
<div class="select__box__select-item" @click="select()"></div>
<el-image v-for="(item, i) in state.list" :key="i + 'l'" class="select__box__select-item" :src="item.thumbUrl" fit="contain" @click="select(item.imgUrl)"></el-image>
</div>
<template #reference>
<el-button class="button" link @click="visiable = !visiable">{{ visiable ? '取消' : '选择' }}</el-button>
<el-button class="button" link @click="state.visiable = !state.visiable">{{ state.visiable ? '取消' : '选择' }}</el-button>
</template>
</el-popover>
</div>
@ -38,58 +38,74 @@
</el-card>
</template>
<script lang="ts">
<script lang="ts" setup>
import api from '@/api'
import { defineComponent, toRefs, reactive, watch, onMounted, nextTick } from 'vue'
import { toRefs, reactive, watch, onMounted, nextTick, defineProps, defineEmits, defineExpose } from 'vue'
import { ElRadioGroup, ElRadioButton } from 'element-plus'
import wSvg from '@/components/modules/widgets/wSvg/wSvg.vue'
import { TGetListResult } from '@/api/material';
export default defineComponent({
components: { ElRadioGroup, ElRadioButton },
props: ['modelValue', 'degree'],
emits: ['change'],
setup(props, context) {
const state = reactive({
// strength: 20, //
effectSelect: '', //
visiable: false, //
type: '166', //
list: [],
})
const select = (value: string = '') => {
state.visiable = false
const setting = JSON.parse(JSON.stringify(wSvg.setting))
setting.svgUrl = value
context.emit('change', setting)
}
onMounted(async () => {
await nextTick()
state.effectSelect = props?.modelValue || ''
// state.strength = props?.degree || state.strength
getList()
})
type TProps = {
modelValue?: string
degree?: number
}
async function getList() {
const res = await api.material.getList({
first_id: 2,
second_id: state.type,
})
state.list = res.list.map(({ thumbUrl, imgUrl }: any) => {
return { thumbUrl, imgUrl }
})
}
watch(
() => state.type,
(value) => {
getList()
},
)
return {
...toRefs(state),
select,
}
type TEmits = {
(event: 'change', data: Record<string, any>): void
}
type TState = {
effectSelect: string
visiable: boolean
type: string
list: {thumbUrl: string, imgUrl: string}[]
}
const props = defineProps<TProps>()
const emit = defineEmits<TEmits>()
const state = reactive<TState>({
// strength: 20, //
effectSelect: '', //
visiable: false, //
type: '166', //
list: [],
})
const select = (value: string = '') => {
state.visiable = false
const setting = JSON.parse(JSON.stringify(wSvg.setting))
setting.svgUrl = value
emit('change', setting)
}
onMounted(async () => {
await nextTick()
state.effectSelect = props?.modelValue || ''
// state.strength = props?.degree || state.strength
getList()
})
async function getList() {
const res = await api.material.getList({
first_id: 2,
second_id: state.type,
})
state.list = res.list.map(({ thumbUrl, imgUrl }) => {
return { thumbUrl, imgUrl }
})
}
watch(
() => state.type,
(value) => {
getList()
},
methods: {},
)
defineExpose({
select,
})
</script>

View File

@ -27,7 +27,7 @@
:key="efi + 'effect'"
:style="{
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,
webkitTextStroke: ef.stroke && ef.stroke.enable ? `${ef.stroke.width / coefficient}px ${ef.stroke.color}` : '',
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,
@ -39,52 +39,61 @@
A
</div>
<span class="title">文字特效</span>
<el-popover :visible="visiable" placement="left" :width="220" trigger="click">
<el-popover :visible="state.visiable" placement="left" :width="220" trigger="click">
<div class="select__box">
<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)">
<div class="select__box__select-item" @click="selectEffect()"></div>
<div v-for="(l, li) in state.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="openSet">{{ visiable ? '取消' : '选择' }}</el-button>
<el-button class="button" link @click="openSet">{{ state.visiable ? '取消' : '选择' }}</el-button>
</template>
</el-popover>
</div>
</template>
<!-- filling 描边 stroke 阴影 shadow -->
<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>
<div v-show="state.layers && state.layers.length > 0" class="text item"><span style="width: 65px">强度</span> <el-slider v-model="state.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>
<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>
<el-button
class="add-layer" size="small" type="primary" link
@click="addLayer">
+ 新建特效层
</el-button>
<el-button
v-show="state.layers && state.layers.length > 0" class="add-layer" size="small"
type="primary" link @click="state.unfold = !state.unfold">
{{ state.unfold ? '收起' : '展开' }}全部
</el-button>
</div>
<div class="line"></div>
<draggable v-model="layers" handle=".sd-yidong" item-key="uuid" v-bind="dragOptions">
<draggable v-model="state.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">
<div v-if="element.filling && [0, 2, '0', '2'].includes(element.filling.type)" v-show="state.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">
<div v-if="element.stroke" v-show="state.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">
<div v-if="element.offset" v-show="state.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">
<div v-if="element.shadow" v-show="state.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" />
@ -98,109 +107,135 @@
</el-card>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, watch, onMounted, nextTick, computed } from 'vue'
<script lang="ts" setup>
import {
reactive, watch, onMounted, nextTick, computed,
defineProps, defineEmits, defineExpose
} 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 = []
let froze_font_effect_list: Record<string, any>[] = []
export default defineComponent({
components: { colorSelect, ElInputNumber, numberInput, ElCheckbox, draggable },
props: ['modelValue', 'degree', 'data'],
emits: ['update:modelValue'],
setup(props, { emit }) {
const state = reactive({
strength: 50, //
visiable: false, //
list: [],
layers: [],
draging: false,
unfold: true,
type TProps = {
modelValue?: Record<string, any>
degree?: string | number
data: Record<string, any>
}
type TEmits = {
(event: 'update:modelValue', data: Record<string, any>[]): void
}
type TState = {
strength: number
visiable: boolean
list: Record<string,any>[]
layers: Record<string, any>[]
draging: boolean
unfold: boolean
}
const props = withDefaults(defineProps<TProps>(), {
modelValue: () => ({}),
data: () => ({})
})
const emit = defineEmits<TEmits>()
const state = reactive<TState>({
strength: 50, //
visiable: false, //
list: [],
layers: [],
draging: false,
unfold: true,
})
const dragOptions = {
animation: 300,
ghostClass: 'ghost',
chosenClass: 'choose',
}
const coefficient = computed(() => Math.round(160 / 27))
let rawData: Record<string, any>[] = [] //
onMounted(async () => {
await nextTick()
// console.log(props.data)
if (!props.data.textEffects) {
return
}
const clone = JSON.parse(JSON.stringify(props.data.textEffects)) || []
state.layers = clone
.map((x: any) => {
x.uuid = String(Math.random())
return x
})
const dragOptions = {
animation: 300,
ghostClass: 'ghost',
chosenClass: 'choose',
}
const coefficient = computed(() => Math.round(160 / 27))
let rawData: any = [] //
.reverse()
rawData = JSON.parse(JSON.stringify(state.layers))
})
onMounted(async () => {
await nextTick()
// console.log(props.data)
if (!props.data.textEffects) {
return
}
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(
() => state.layers,
(v) => {
const newEffect = v.map((x) => {
delete x.uuid
return x
})
watch(
() => state.layers,
(v) => {
const newEffect = v.map((x) => {
delete x.uuid
return x
})
emit('update:modelValue', newEffect.reverse())
},
{ deep: true },
)
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 selectEffect = async (id?: number) => {
state.visiable = false
if (id) {
const { data } = await api.home.getTempDetail({ id, type: 1 })
state.layers = JSON.parse(data)
.textEffects.map((x: Record<string, any>) => {
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 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 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 finish = (type?: string, value?: string) => {}
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 colorChange = (e: Record<string, any>, item: Record<string, any>) => {
const modeStr: Record<string, number> = {
渐变: 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
@ -223,35 +258,32 @@ export default defineComponent({
})
}
//
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),
selectEffect,
finish,
coefficient,
removeLayer,
addLayer,
dragOptions,
onDone,
strengthChange,
openSet,
colorChange,
getGradientOrImg,
}
},
methods: {},
//
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
}
defineExpose({
selectEffect,
finish,
coefficient,
removeLayer,
addLayer,
dragOptions,
onDone,
strengthChange,
openSet,
colorChange,
getGradientOrImg,
})
</script>

View File

@ -6,128 +6,137 @@
<div class="content">
<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="modes" @change="colorChange" @nativePick="dropColor" />
<color-picker v-model:value="state.innerColor" :modes="modes" @change="colorChange" @nativePick="dropColor" />
<template #reference>
<div class="color__bar" :style="{ background: innerColor }"></div>
<div class="color__bar" :style="{ background: state.innerColor }"></div>
</template>
</el-popover>
</div>
</div>
</template>
<script lang="ts">
const NAME = 'color-select'
import { defineComponent, toRefs, reactive, computed, onMounted, watch } from 'vue'
<script lang="ts" setup>
// const NAME = 'color-select'
import {reactive, onMounted, watch } from 'vue'
import { useStore } from 'vuex'
// import { debounce } from 'throttle-debounce'
// import { toolTip } from '@/common/methods/helper'
// import colorPicker from '@/utils/plugins/color-picker/index.vue'
import colorPicker from '@palxp/color-picker'
export default defineComponent({
name: NAME,
components: { colorPicker },
inheritAttrs: false,
props: {
label: {
default: '',
},
modelValue: {
default: '',
},
width: {
default: '100%',
},
modes: {
default: () => ['纯色'],
},
},
emits: ['finish', 'update:modelValue', 'change'],
setup(props, { emit }) {
const store = useStore()
const state: any = reactive({
innerColor: '#ffffffff',
// colorLength: 0,
// hasEyeDrop: 'EyeDropper' in window,
})
let first = true
type TProps = {
label?: string
modelValue?: string
width?: string
modes?: string[]
}
type TEmits = {
(event: 'finish', data: string): void
(event: 'update:modelValue', data: string): void
(event: 'change', data: string): void
}
type TState = {
innerColor: string
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
modelValue: '',
width: '100%',
modes: () => (['纯色'])
})
const emit = defineEmits<TEmits>()
const store = useStore()
const state = reactive<TState>({
innerColor: '',
// colorLength: 0,
// hasEyeDrop: 'EyeDropper' in window,
})
let first = true
// const dColorHistory = computed(() => {
// return store.getters.dColorHistory
// })
onMounted(() => {
if (props.modelValue) {
let fixColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
// @palxp/color-picker16
state.innerColor = fixColor.toLocaleUpperCase()
}
})
const dropColor = async (e: any) => {
console.log('取色: ', e)
}
onMounted(() => {
if (props.modelValue) {
let fixColor = props.modelValue + (props.modelValue.length === 7 ? 'ff' : '')
// @palxp/color-picker16
state.innerColor = fixColor.toLocaleUpperCase()
}
})
watch(
() => state.innerColor,
(value) => {
activeChange(value)
if (first) {
first = false
return
}
// addHistory(value)
},
)
watch(
() => props.modelValue,
(val) => {
val !== state.innerColor && (state.innerColor = val)
},
)
const dropColor = async (color: string) => {
console.log('取色: ', color)
}
const updateValue = (value: any) => {
emit('update:modelValue', value)
}
const activeChange = (value: any) => {
updateValue(value)
}
const onChange = () => {
emit('finish', state.innerColor)
}
// const addHistory = debounce(300, false, async (value) => {
// store.dispatch('pushColorToHistory', value)
// })
// const colorChange = debounce(150, false, async (e) => {
// state.innerColor = e + (e.length === 7 ? 'ff' : '')
// })
const inputBlur = (color: string) => {
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,
activeChange,
onChange,
dropColor,
inputBlur,
enter,
hide,
colorChange,
watch(
() => state.innerColor,
(value) => {
activeChange(value)
if (first) {
first = false
return
}
// addHistory(value)
},
)
watch(
() => props.modelValue,
(val) => {
val !== state.innerColor && (state.innerColor = val)
},
)
const updateValue = (value: any) => {
emit('update:modelValue', value)
}
const activeChange = (value: any) => {
updateValue(value)
}
const onChange = () => {
emit('finish', state.innerColor)
}
// const addHistory = debounce(300, false, async (value) => {
// store.dispatch('pushColorToHistory', value)
// })
// const colorChange = debounce(150, false, async (e) => {
// state.innerColor = e + (e.length === 7 ? 'ff' : '')
// })
const inputBlur = (color: string) => {
state.innerColor = color
}
const enter = () => {
store.commit('setShowMoveable', false) //
}
const hide = () => {
store.commit('setShowMoveable', true) //
}
const colorChange = (color: string) => {
emit('change', color)
}
defineExpose({
// dColorHistory,
activeChange,
onChange,
dropColor,
inputBlur,
enter,
hide,
colorChange,
})
</script>

View File

@ -18,34 +18,40 @@
</div>
</template>
<script>
<script lang="ts" setup>
//
const NAME = 'icon-item-select'
// const NAME = 'icon-item-select'
export default {
name: NAME,
props: {
label: {
default: '',
},
data: {
required: true,
type: Array,
},
},
emits: ['finish'],
data() {
return {}
},
methods: {
selectItem(item) {
if (typeof item.select !== 'undefined') {
item.select = !item.select
}
this.$emit('finish', item)
},
},
type TPropData = {
select: boolean,
extraIcon: boolean,
tip: string
icon?: string
}
type TProps = {
label?: string
data: TPropData[]
}
type TEmits = {
(event: 'finish', data: TPropData): void
}
const props = withDefaults(defineProps<TProps>(), {
label: ''
})
const emit = defineEmits<TEmits>()
function selectItem(item: TPropData) {
if (typeof item.select !== 'undefined') {
item.select = !item.select
}
emit('finish', item)
}
</script>
<style lang="less" scoped>

View File

@ -13,11 +13,26 @@
</div> -->
<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)" />
<input
:class="{ 'small-input': true, disable: !editable }" type="text" :value="modelValue"
:readonly="!props.editable"
@input="updateValue($event && $event.target ? ($event.target as HTMLInputElement).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 class="input-wrap">
<input
:class="{ 'real-input': true, disable: !editable }"
type="text" :value="modelValue" :readonly="!props.editable"
@input="updateValue($event && $event.target ? ($event.target as HTMLInputElement).value : '')"
@focus="focusInput"
@blur="blurInput"
@keyup="verifyNumber"
@keydown="(e) => opNumber(e)"
/>
</div>
<span style="color: rgba(0, 0, 0, 0.45)">{{ label }}</span>
<!-- <div :class="{ 'input-wrap': true, active: inputBorder }">
@ -30,125 +45,130 @@
</div>
</template>
<script>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
//
const NAME = 'number-input'
// const NAME = 'number-input'
export default {
name: NAME,
props: {
label: {
default: '',
},
modelValue: {
default: '',
},
editable: {
default: true,
},
step: {
default: 1,
},
maxValue: {},
minValue: {},
type: {},
prepend: {},
},
emits: ['finish', 'update:modelValue'],
data() {
return {
inputBorder: false,
tagText: '',
showEdit: false,
type TProps = {
label?: string
modelValue?: string | number
editable?: boolean
step?: number
maxValue?: string | number
minValue?: string | number
type?: string
prepend?: string
}
type TEmits = {
(event: 'finish', data: number | string): void
(event: 'update:modelValue', data: number | string): void
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
modelValue: '',
editable: true,
step: 1
})
const emit = defineEmits<TEmits>()
const inputBorder = ref<boolean>(false)
const tagText = ref<string | number>('')
const showEdit = ref<boolean>(false)
watch(
() => props.modelValue,
() => fixedNum()
)
onMounted(() => {
fixedNum()
})
function fixedNum() {
//
const decimal = String(props.modelValue).split('.')[1]
if (decimal && decimal.length > 2) {
setTimeout(() => {
updateValue(Number(props.modelValue).toFixed(2))
}, 10)
}
//
if (props.maxValue && props.modelValue > props.maxValue) {
setTimeout(() => {
updateValue(Number(props.maxValue))
}, 10)
} else if (typeof props.minValue === 'number' && Number(props.modelValue) < Number(props.minValue)) {
setTimeout(() => {
updateValue(Number(props.minValue))
}, 10)
}
}
function updateValue(value: string | number) {
emit('update:modelValue', value === '-' ? '-' : Number(value))
}
function up() {
updateValue(parseInt(`${props.modelValue}` ?? '0', 10) + props.step)
}
function down() {
let value = parseInt(`${props.modelValue}` ?? '0', 10) - props.step
updateValue(value)
}
function opNumber(e: KeyboardEvent) {
e.stopPropagation()
switch (e.keyCode) {
case 38:
up()
return
case 40:
down()
return
}
}
function verifyNumber() {
let value = String(props.modelValue)
let len = value.length
let newValue = ''
let isNegative = value[0] === '-'
// 0
for (let i = isNegative ? 1 : 0; i < len; ++i) {
let c = value[i]
if (c == '.' || (c >= '0' && c <= '9')) {
newValue += c
} else {
break
}
},
computed: {},
watch: {
modelValue() {
this.fixedNum()
},
},
mounted() {
this.fixedNum()
},
methods: {
fixedNum() {
//
const decimal = String(this.modelValue).split('.')[1]
if (decimal && decimal.length > 2) {
setTimeout(() => {
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 === '-' ? '-' : Number(value))
},
up() {
this.updateValue(parseInt(this.modelValue || 0, 10) + this.step)
},
down() {
let value = parseInt(this.modelValue || 0, 10) - this.step
this.updateValue(value)
},
opNumber(e) {
e.stopPropagation()
switch (e.keyCode) {
case 38:
this.up()
return
case 40:
this.down()
return
}
},
verifyNumber() {
let value = String(this.modelValue)
let len = value.length
let newValue = ''
let isNegative = value[0] === '-'
// 0
for (let i = isNegative ? 1 : 0; i < len; ++i) {
let c = value[i]
if (c == '.' || (c >= '0' && c <= '9')) {
newValue += c
} else {
break
}
}
if (newValue === '') {
newValue = '0'
}
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)
}
},
},
}
if (newValue === '') {
newValue = '0'
}
if (isNegative) {
newValue = '-' + (newValue === '0' ? '' : newValue)
}
updateValue(newValue)
// this.updateValue(parseInt(newValue, 10))
}
function focusInput() {
inputBorder.value = true
tagText.value = props.modelValue
}
function blurInput() {
if (props.modelValue === '-') {
updateValue(0)
}
inputBorder.value = false
if (props.modelValue !== tagText.value) {
emit('finish', props.modelValue)
}
}
</script>

View File

@ -8,68 +8,71 @@
<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="showInput" :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"
/>
</div>
</template>
<script>
const NAME = 'number-slider'
import { mapGetters, mapActions } from 'vuex'
<script lang="ts" setup>
// const NAME = 'number-slider'
import { watch, ref, onMounted } from 'vue';
import { mapActions } from 'vuex'
export default {
name: NAME,
props: {
label: {
default: '',
},
labelWidth: {
default: '71px',
},
modelValue: {
default: 0,
},
minValue: {
default: 0,
},
maxValue: {
default: 500,
},
step: {
default: 1,
},
showInput: {
default: true,
},
},
emits: ['update:modelValue', 'finish'],
data() {
return {
innerValue: 0,
// first: true,
type TProps = {
label?: string
labelWidth?: string
modelValue?: number
minValue?: number
maxValue?: number
step?: number
showInput?: boolean
}
type TEmits = {
(event: 'update:modelValue', data: number): void
(event: 'finish', data: number | number[]): void
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
labelWidth: '71px',
modelValue: 0,
minValue: 0,
maxValue: 500,
step: 1,
showInput: true
})
const emit = defineEmits<TEmits>()
const innerValue = ref<number>(0)
watch(
() => innerValue.value,
(value) => {
if (props.modelValue !== value) {
emit('update:modelValue', value)
}
},
computed: {
...mapGetters([]),
},
watch: {
innerValue(value) {
if (this.modelValue !== value) {
this.$emit('update:modelValue', value)
}
},
modelValue(val) {
this.innerValue = this.modelValue
},
},
created() {
this.innerValue = this.modelValue
},
methods: {
...mapActions([]),
changeValue(value) {
this.$emit('finish', value)
},
},
}
)
watch(
() => props.modelValue,
() => {
innerValue.value = props.modelValue
}
)
onMounted(() => {
innerValue.value = props.modelValue
})
function changeValue(value: number | number[]) {
emit('finish', value)
}
</script>

View File

@ -9,51 +9,59 @@
<div class="text-input">
<p v-if="label" class="input-label">{{ label }}</p>
<div :class="{ 'input-wrap': true, active: inputBorder }">
<input :class="{ 'real-input': true, disable: !editable }" type="text" :value="modelValue" :readonly="editable ? false : 'readonly'" @input="updateValue($event.target.value)" @focus="focusInput" @blur="blurInput" />
<input
:class="{ 'real-input': true, disable: !props.editable }"
type="text" :value="props.modelValue"
:readonly="!editable"
@input="updateValue(($event.target as HTMLInputElement).value)"
@focus="focusInput"
@blur="blurInput"
/>
</div>
</div>
</template>
<script>
<script lang="ts" setup>
import { ref } from 'vue'
//
const NAME = 'text-input'
// const NAME = 'text-input'
export default {
name: NAME,
props: {
label: {
default: '',
},
modelValue: {
default: '',
},
editable: {
default: true,
},
},
emits: ['update:modelValue', 'finish'],
data() {
return {
inputBorder: false,
tagText: '',
}
},
computed: {},
methods: {
updateValue(value) {
this.$emit('update:modelValue', value)
},
focusInput() {
this.inputBorder = true
this.tagText = this.modelValue
},
blurInput() {
this.inputBorder = false
if (this.modelValue !== this.tagText) {
this.$emit('finish', this.modelValue)
}
},
},
type TProps = {
label?: string
modelValue?: string
editable?: boolean
}
type TEmits = {
(event:'update:modelValue', data: string): void
(event: 'finish', data: string): void
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
modelValue: '',
editable: true,
})
const emit = defineEmits<TEmits>()
const inputBorder = ref(false)
const tagText = ref<string>('')
function updateValue(value: string) {
emit('update:modelValue', value)
}
function focusInput() {
inputBorder.value = true
tagText.value = props.modelValue
}
function blurInput() {
inputBorder.value = false
if (props.modelValue !== tagText.value) {
emit('finish', props.modelValue)
}
}
</script>

View File

@ -9,61 +9,65 @@
<div id="text-input-area">
<p v-if="label" class="input-label">{{ label }}</p>
<div :class="{ 'input-wrap': true, active: inputBorder }">
<textarea :maxlength="max" :class="{ 'real-input': true, disable: !editable }" type="text" rows="3" :value="dealValue" :readonly="editable ? false : 'readonly'" @input="updateValue($event.target.value)" @focus="focusInput" @blur="blurInput" />
<textarea
:maxlength="max" :class="{ 'real-input': true, disable: !editable }"
type="text" rows="3" :value="dealValue"
:readonly="!editable" @input="updateValue(($event.target as HTMLTextAreaElement).value)"
@focus="focusInput" @blur="blurInput"
/>
</div>
</div>
</template>
<script>
<script lang="ts" setup>
import { ref, computed } from 'vue'
//
const NAME = 'text-input-area'
// const NAME = 'text-input-area'
export default {
name: NAME,
props: {
label: {
default: '',
},
modelValue: {
default: '',
},
editable: {
default: true,
},
max: {},
},
emits: ['update:modelValue', 'finish'],
data() {
return {
inputBorder: false,
tagText: '',
}
},
computed: {
dealValue() {
return this.modelValue
// return this.modelValue.replace(/<br\/>/g, '\r\n').replace(/&nbsp;/g, ' ')
},
},
methods: {
updateValue(value) {
this.$emit('update:modelValue', this.getValue(value))
},
focusInput() {
this.inputBorder = true
this.tagText = this.modelValue
},
blurInput() {
this.inputBorder = false
let v = this.getValue(this.modelValue)
if (v !== this.tagText) {
this.$emit('finish', v)
}
},
getValue(value) {
return value.replace(/\n|\r\n/g, '<br/>').replace(/ /g, '&nbsp;')
},
},
type TProps = {
label?: string
modelValue?: string
editable?: boolean
max?: string
}
type TEmits = {
(event:'update:modelValue', data: string): void
(event: 'finish', data: string): void
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
modelValue: '',
editable: true,
})
const emit = defineEmits<TEmits>()
const inputBorder = ref(false)
const tagText = ref<string>('')
const dealValue = computed(() => {
return props.modelValue
})
function updateValue(value: string) {
emit('update:modelValue', getValue(value))
}
function focusInput() {
inputBorder.value = true
tagText.value = props.modelValue
}
function blurInput() {
inputBorder.value = false
let v = getValue(props.modelValue)
if (v !== tagText.value) {
emit('finish', v)
}
}
function getValue(value: string) {
return value.replace(/\n|\r\n/g, '<br/>').replace(/ /g, '&nbsp;')
}
</script>

View File

@ -13,18 +13,22 @@
<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)">
<img v-if="listItem.preview" class="preview" :src="listItem.preview" />
<li
v-for="listItem in data" :key="typeof listItem === 'object' ? listItem.alias : listItem"
:class="{ active: listItem == state.innerValue }"
@click="selectItem(listItem)"
>
<img v-if="listItem.preview" class="preview" :src="listItem.preview" alt="preview" />
<span v-else>{{ (typeof listItem === 'object' ? listItem.alias : listItem) + suffix }}</span>
</li>
</ul>
<!-- tab分类列表 -->
<div v-else class="tabs-wrap">
<el-tabs v-model="activeTab">
<el-tabs v-model="state.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" />
<li v-for="listItem in data[key]" :key="typeof listItem === 'object' ? listItem.alias : listItem" :class="{ active: listItem == state.innerValue }" @click="selectItem(listItem)">
<img v-if="listItem.preview" class="preview" :src="listItem.preview" alt="preview" />
<span v-else :style="{ fontFamily: `'${listItem.value}'` }">{{ (typeof listItem === 'object' ? listItem.alias : listItem) + suffix }}</span>
</li>
</ul>
@ -32,9 +36,16 @@
</el-tabs>
</div>
<template #reference>
<div :class="['input-wrap', { active: inputBorder }]" :style="{ width: inputWidth }">
<div :class="['input-wrap', { active: state.inputBorder }]" :style="{ width: inputWidth }">
<!-- <img v-if="innerPreview" class="preview" :src="innerPreview" /> -->
<input :style="{ fontFamily: modelValue.value }" :class="['real-input', { disable: !disable }]" :readonly="readonly ? 'readonly' : false" type="text" :value="showValue" @input="inputText" @focus="inputBorder = true" @blur="inputBorder = false" @keydown="(e) => opNumber(e)" />
<input
:style="{ fontFamily: modelValue.value }"
:class="['real-input', { disable: !disable }]"
:readonly="readonly" type="text"
:value="showValue"
@input="inputText" @focus="state.inputBorder = true"
@blur="state.inputBorder = false" @keydown="(e) => opNumber(e)"
/>
<!-- <span class="input-unit">{{ suffix }}</span> -->
<div class="op-btn">
<!-- <div class="down" @click="inputBorder = !inputBorder"></div> -->
@ -46,124 +57,127 @@
</div>
</template>
<script>
<script lang="ts" setup>
//
const NAME = 'value-input'
import { ElTabPane, ElTabs } from 'element-plus'
import { computed, onMounted, reactive, ref, watch } from 'vue';
export default {
name: NAME,
components: { ElTabPane, ElTabs },
props: {
label: {
default: '',
},
modelValue: {
default: '',
},
suffix: {
default: '',
},
data: {
required: true,
},
disable: {
default: true,
},
inputWidth: {
default: '80px',
},
// textAlign: {
// default: 'center',
// },
readonly: {
default: false,
},
step: {
default: 1,
},
},
emits: ['finish', 'update:modelValue'],
data() {
return {
inputBorder: false,
tagText: '',
width: '0',
innerValue: '',
innerPreview: '',
activeTab: '中文',
type TProps = {
label?: string
modelValue?: Record<string, any>
suffix?: string
data: Record<string, any>
disable?: boolean
inputWidth?: string
readonly?: boolean
step?: number
}
type TEmits = {
(event:'update:modelValue', data: Record<string, any> | string | number): void
(event: 'finish', data: Record<string, any> | string | number): void
}
type TState = {
inputBorder: boolean
tagText: string
width: string | number
innerValue: string
innerPreview: string
activeTab: string
}
const props = withDefaults(defineProps<TProps>(), {
label: '',
modelValue: () => ({}),
suffic: '',
data: () => ({}),
disable: true,
inputWidth: '80px',
readonly: false,
step: 1,
})
const emit = defineEmits<TEmits>()
const state = reactive<TState>({
inputBorder: false,
tagText: '',
width: '0',
innerValue: '',
innerPreview: '',
activeTab: '中文',
})
const selectRef = ref<HTMLElement | null>(null)
const showValue = computed(() => {
return state.innerValue
})
watch(
() => props.modelValue,
() => {
state.innerValue = typeof props.modelValue === 'object' ? props.modelValue.alias : props.modelValue
}
)
watch(
() => state.inputBorder,
(value) => {
if (value) {
state.tagText = state.innerValue
} else {
if (state.innerValue !== state.tagText) {
emit('finish', state.innerValue)
}
}
},
computed: {
showValue() {
// return this.innerValue + this.suffix
return this.innerValue
},
},
watch: {
modelValue(value) {
this.innerValue = typeof this.modelValue === 'object' ? this.modelValue.alias : this.modelValue
},
inputBorder(value) {
if (value) {
this.tagText = this.innerValue
} else {
if (this.innerValue !== this.tagText) {
this.$emit('finish', this.innerValue)
}
}
},
},
created() {
this.innerValue = typeof this.modelValue === 'object' ? this.modelValue.alias : this.modelValue
},
mounted() {
this.width = this.$refs.select.offsetWidth
// if (Object.prototype.toString.call(this.data) === '[Object Object]') {
// for (const key in this.data) {
// console.log(key)
// break
// }
// }
},
methods: {
selectItem(item) {
let value = typeof item === 'object' ? item.alias : item
if (this.innerValue !== value) {
this.innerValue = value
this.innerPreview = item.preview
this.$emit('finish', item)
}
},
inputText(e) {
// this.innerValue = e.target.value.replace(RegExp(this.suffix), '')
this.innerValue = e.target.value
setTimeout(() => {
this.$emit('finish', this.innerValue)
}, 100)
},
opNumber(e) {
e.stopPropagation()
switch (e.keyCode) {
case 38:
typeof this.innerValue === 'number' && this.up()
return
case 40:
typeof this.innerValue === 'number' && this.down()
return
}
},
up() {
this.$emit('update:modelValue', parseInt(this.modelValue || 0, 10) + this.step)
},
down() {
let value = parseInt(this.modelValue || 0, 10) - this.step
if (value < 0) {
value = 0
}
this.$emit('update:modelValue', value)
},
},
}
)
onMounted(() => {
state.innerValue = typeof props.modelValue === 'object' ? props.modelValue.alias : props.modelValue
if (selectRef.value) {
state.width = selectRef.value.offsetWidth
}
})
function selectItem(item: Record<string, any>) {
let value = typeof item === 'object' ? item.alias : item
if (state.innerValue !== value) {
state.innerValue = value
state.innerPreview = item.preview
emit('finish', item)
}
}
function inputText(e: Event) {
// this.innerValue = e.target.value.replace(RegExp(this.suffix), '')
state.innerValue = (e.target as HTMLInputElement).value
setTimeout(() => {
emit('finish', state.innerValue)
}, 100)
}
function opNumber(e: KeyboardEvent) {
e.stopPropagation()
switch (e.keyCode) {
case 38:
typeof state.innerValue === 'number' && up()
return
case 40:
typeof state.innerValue === 'number' && down()
return
}
}
function up() {
emit('update:modelValue', parseInt(`${props.modelValue}` ?? '0', 10) + props.step)
}
function down() {
let value = parseInt(`${props.modelValue}` ?? '0', 10) - props.step
if (value < 0) {
value = 0
}
emit('update:modelValue', value)
}
</script>

View File

@ -0,0 +1,23 @@
export const wGroupSetting = {
name: '组合',
type: 'w-group',
uuid: -1,
width: 0,
height: 0,
left: 0,
top: 0,
transform: '',
opacity: 1,
parent: '-1',
isContainer: true,
record: {
width: 0,
height: 0,
minWidth: 0,
minHeight: 0,
dir: 'none',
},
}

View File

@ -8,11 +8,11 @@
<template>
<div
ref="widget"
:class="['w-group', { 'layer-lock': params.lock }]"
:class="['w-group', { 'layer-lock': props.params?.lock }]"
:style="{
position: 'absolute',
left: params.left - parent.left + 'px',
top: params.top - parent.top + 'px',
left: (props.params.left || 0) - (props.parent?.left || 0) + 'px',
top: (props.params.top || 0) - (props.parent.top || 0) + 'px',
width: params.width + 'px',
height: params.height + 'px',
opacity: params.opacity,
@ -22,184 +22,223 @@
</div>
</template>
<script>
<script lang="ts" setup>
//
const NAME = 'w-group'
import { mapGetters, mapActions } from 'vuex'
import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref } from 'vue'
import { mapGetters, mapActions, useStore } from 'vuex'
import { setTransformAttribute } from '@/common/methods/handleTransform'
import { useSetupMapGetters } from '@/common/hooks/mapGetters';
export default {
name: NAME,
setting: {
name: '组合',
type: NAME,
uuid: -1,
type TParamsData = {
left: number
top: number
width: number
height: number
opacity: number
rotate: number
uuid: string
lock: boolean
fontSize: number
}
type TProps = {
params?: Partial<TParamsData>
parent?: Partial<Pick<TParamsData, "top" | "left">>
}
const props = withDefaults(defineProps<TProps>(), {
params: () => ({}),
parent: () => ({})
})
const store = useStore();
const widget = ref<HTMLElement | null>(null)
const ratio = ref(0)
const temp = ref<Record<string, any>>({})
const compWidgetsRecord = ref<Record<string, any>>({})
const setting = {
name: '组合',
type: NAME,
uuid: -1,
width: 0,
height: 0,
left: 0,
top: 0,
transform: '',
opacity: 1,
parent: '-1',
isContainer: true,
record: {
width: 0,
height: 0,
left: 0,
top: 0,
transform: '',
opacity: 1,
parent: '-1',
isContainer: true,
record: {
width: 0,
height: 0,
minWidth: 0,
minHeight: 0,
dir: 'none',
},
},
props: ['params', 'parent'],
data() {
return {
// loading: false,
timer: null,
}
},
computed: {
...mapGetters(['dActiveElement', 'dWidgets']),
},
// watch: {
// params: {
// async handler(nval) {
// this.updateRecord(nval.tempScale)
// },
// immediate: true,
// deep: true,
// },
// },
updated() {
this.updateRecord()
},
async mounted() {
await this.$nextTick()
this.touchstart()
this.updateRecord()
document.addEventListener('mousedown', this.touchstart, false)
document.addEventListener('mouseup', this.touchend, false)
this.params.rotate && (this.$refs.widget.style.transform += `rotate(${this.params.rotate})`)
},
beforeUnmount() {
document.removeEventListener('mousedown', this.touchstart, false)
document.removeEventListener('mouseup', this.touchend, false)
},
methods: {
...mapActions(['updateWidgetData']),
updateRecord(tempScale) {
if (this.dActiveElement.uuid === this.params.uuid) {
// clearTimeout(this.timer)
let record = this.dActiveElement.record
if (record.width <= 0) {
this.touchend()
}
// if (this.tempRecord && this.tempRecord.width && this.tempRecord.width != record.width) {
// return
// }
this.ratio = tempScale || this.params.width / record.width
if (this.ratio != 1) {
this.temp = {}
if (record.width != 0) {
for (let i = this.dWidgets.length - 1; i >= 0; --i) {
if (this.dWidgets[i].parent === this.params.uuid) {
this.temp[this.dWidgets[i].uuid] = { width: this.dWidgets[i].width * this.ratio, height: this.dWidgets[i].height * this.ratio, raw: this.dWidgets[i] }
}
}
}
// TODO DOM Change
// this.dActiveElement.scale = this.ratio
this.$refs.widget.style.transformOrigin = 'left top' // scale
setTransformAttribute(this.$refs.widget, 'scale', this.ratio)
// this.timer = setTimeout(() => {
// this.touchend()
// }, 300)
}
}
},
touchstart() {
if (this.dActiveElement.uuid !== this.params.uuid) {
return
}
this.tempRecord = {
width: this.params.width,
height: this.params.height,
}
this.compWidgetsRecord = {}
for (let i = this.dWidgets.length - 1; i >= 0; --i) {
if (this.dWidgets[i].parent === this.params.uuid) {
this.compWidgetsRecord[this.dWidgets[i].uuid] = {
left: Number(document.getElementById(this.dWidgets[i].uuid).style.left.replace('px', '')),
top: Number(document.getElementById(this.dWidgets[i].uuid).style.top.replace('px', '')),
fontSize: Number(document.getElementById(this.dWidgets[i].uuid).style.fontSize?.replace('px', '')),
}
}
}
},
touchend() {
if (this.dActiveElement.uuid !== this.params.uuid) {
return
}
// const opacity = this.$refs.widget.style.opacity
// this.$refs.widget.style.opacity = 1
setTimeout(() => {
if (!this.temp) {
return
}
this.$refs.widget.style.opacity = 0
setTransformAttribute(this.$refs.widget, 'scale', 1)
setTimeout(() => {
this.$refs.widget.style.opacity = this.params.opacity
// this.$refs.widget.style.transformOrigin = 'center' // scale
}, 100)
// const opacity = this.$refs.widget.style.opacity
// setTransformAttribute(this.$refs.widget, 'scale', 1)
for (const key in this.temp) {
if (Object.hasOwnProperty.call(this.temp, key)) {
this.keyChange(key, 'width', this.temp[key].width)
this.keyChange(key, 'height', this.temp[key].height)
// DOM
this.keySetValue(key, 'left', this.compWidgetsRecord[key].left * this.ratio)
this.keySetValue(key, 'top', this.compWidgetsRecord[key].top * this.ratio)
// this.keySetValue(key, 'left', Number(document.getElementById(key).style.left.replace('px', '')) * this.ratio)
// this.keySetValue(key, 'top', Number(document.getElementById(key).style.top.replace('px', '')) * this.ratio)
if (this.temp[key].raw.type === 'w-text') {
this.keyChange(key, 'fontSize', this.compWidgetsRecord[key].fontSize * this.ratio)
// this.keyChange(key, 'fontSize', this.temp[key].raw.fontSize * this.ratio)
// this.keyChange(key, 'letterSpacing', this.temp[key].raw.letterSpacing * this.ratio)
}
}
}
// this.$refs.widget.style.opacity = opacity
this.temp = null
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.dActiveElement.width = this.$refs.widget?.offsetWidth
this.dActiveElement.height = this.$refs.widget?.offsetHeight
}
}, 10)
},
keyChange(uuid, key, value) {
this.updateWidgetData({
uuid,
key,
value,
pushHistory: false,
})
},
keySetValue(uuid, key, value) {
setTimeout(() => {
const widget = this.dWidgets.find((item) => item.uuid === uuid)
widget[key] = value + Number(this.params[key])
}, 10)
},
minWidth: 0,
minHeight: 0,
dir: 'none',
},
}
const timer = ref<number | null>(null)
const { dActiveElement, dWidgets } = useSetupMapGetters(['dActiveElement', 'dWidgets'])
// watch: {
// params: {
// async handler(nval) {
// this.updateRecord(nval.tempScale)
// },
// immediate: true,
// deep: true,
// },
// },
onUpdated(() => {
updateRecord()
})
onMounted(async () => {
await nextTick()
touchstart()
updateRecord()
document.addEventListener('mousedown', touchstart, false)
document.addEventListener('mouseup', touchend, false)
if (props.params?.rotate && widget.value) {
(widget.value.style.transform += `rotate(${props.params.rotate})`)
}
})
onBeforeUnmount(() => {
document.removeEventListener('mousedown', touchstart, false)
document.removeEventListener('mouseup', touchend, false)
})
// ...mapActions(['updateWidgetData']),
function updateRecord(tempScale ?: number) {
if (dActiveElement.value.uuid === props.params.uuid) {
// clearTimeout(this.timer)
let record = dActiveElement.value.record
if (record.width <= 0) {
touchend()
}
// if (this.tempRecord && this.tempRecord.width && this.tempRecord.width != record.width) {
// return
// }
ratio.value = tempScale || (props.params.width || 0) / record.width
if (ratio.value != 1) {
if (record.width != 0) {
for (let i = dWidgets.value.length - 1; i >= 0; --i) {
if (dWidgets.value[i].parent === props.params.uuid) {
temp.value[dWidgets.value[i].uuid] = { width: dWidgets.value[i].width * ratio.value, height: dWidgets.value[i].height * ratio.value, raw: dWidgets.value[i] }
}
}
}
// TODO DOM Change
// this.dActiveElement.scale = this.ratio
if (widget.value) {
widget.value.style.transformOrigin = 'left top' // scale
setTransformAttribute(widget.value, 'scale', ratio.value)
}
// this.timer = setTimeout(() => {
// this.touchend()
// }, 300)
}
}
}
function touchstart() {
if (dActiveElement.value.uuid !== props.params.uuid) {
return
}
const tempRecord = {
width: props.params.width,
height: props.params.height,
}
for (let i = dWidgets.value.length - 1; i >= 0; --i) {
if (dWidgets.value[i].parent === props.params.uuid) {
const el = document.getElementById(dWidgets.value[i].uuid)
if (el) {
compWidgetsRecord.value[dWidgets.value[i].uuid] = {
left: Number(el.style.left.replace('px', '')),
top: Number(el.style.top.replace('px', '')),
fontSize: Number(el.style.fontSize?.replace('px', '')),
}
}
}
}
}
function touchend() {
if (dActiveElement.value.uuid !== props.params.uuid) {
return
}
// const opacity = this.$refs.widget.style.opacity
// this.$refs.widget.style.opacity = 1
setTimeout(() => {
if (!temp.value || !widget.value) {
return
}
widget.value.style.opacity = `${0}`
setTransformAttribute(widget.value, 'scale', 1)
setTimeout(() => {
if (!widget.value) return
widget.value.style.opacity = `${props.params.opacity}`
// this.$refs.widget.style.transformOrigin = 'center' // scale
}, 100)
// const opacity = this.$refs.widget.style.opacity
// setTransformAttribute(this.$refs.widget, 'scale', 1)
for (const key in temp.value) {
if (Object.hasOwnProperty.call(temp.value, key)) {
keyChange(key, 'width', temp.value[key].width)
keyChange(key, 'height', temp.value[key].height)
// DOM
keySetValue(key, 'left', compWidgetsRecord.value[key].left * ratio.value)
keySetValue(key, 'top', compWidgetsRecord.value[key].top * ratio.value)
// this.keySetValue(key, 'left', Number(document.getElementById(key).style.left.replace('px', '')) * this.ratio)
// this.keySetValue(key, 'top', Number(document.getElementById(key).style.top.replace('px', '')) * this.ratio)
if (temp.value[key].raw.type === 'w-text') {
keyChange(key, 'fontSize', compWidgetsRecord.value[key].fontSize * ratio.value)
// this.keyChange(key, 'fontSize', this.temp[key].raw.fontSize * this.ratio)
// this.keyChange(key, 'letterSpacing', this.temp[key].raw.letterSpacing * this.ratio)
}
}
}
// this.$refs.widget.style.opacity = opacity
temp.value = {}
if (dActiveElement.value.uuid === props.params.uuid) {
let record = dActiveElement.value.record
record.width = widget.value?.offsetWidth
record.height = widget.value?.offsetHeight
dActiveElement.value.width = widget.value?.offsetWidth
dActiveElement.value.height = widget.value?.offsetHeight
}
}, 10)
}
function keyChange(uuid: string, key: keyof TParamsData, value: number) {
store.dispatch('updateWidgetData', {
uuid,
key,
value,
pushHistory: false,
})
// updateWidgetData({
// uuid,
// key,
// value,
// pushHistory: false,
// })
}
function keySetValue(uuid: string, key: keyof TParamsData, value: number) {
setTimeout(() => {
const widget = dWidgets.value.find((item: TParamsData) => item.uuid === uuid)
widget[key] = value + Number(props.params[key] || '')
}, 10)
}
defineExpose({
setting
})
</script>
<style lang="less" scoped>

View File

@ -20,3 +20,7 @@ export default {
QINIUYUN_PLUGIN: 'https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/qiniu-js/2.5.5/qiniu.min.js',
supportSubFont: true, // 是否开启服务端字体压缩
}
export const LocalStorageKey = {
tokenKey: "xp_token"
}

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2022-03-03 14:13:16
* @Description:
* @LastEditors: ShawnPhang <site: book.palxp.com>
* @LastEditTime: 2023-06-29 15:11:46
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-02-26 17:54:00
*/
import { createApp } from 'vue'
import App from './App.vue'
@ -20,7 +20,7 @@ elementConfig.components.forEach((component) => {
app.component(component.name, component)
})
elementConfig.plugins.forEach((plugin: any) => {
elementConfig.plugins.forEach((plugin) => {
app.use(plugin)
})

View File

@ -48,7 +48,7 @@ const move = {
const moveInit = {
methods: {
initmovement(e: any) {
initmovement(e: MouseEvent) {
if (!store.getters.dAltDown) {
// 设置mouseevent给moveable初始
// 在组合操作时排除

View File

@ -14,6 +14,7 @@ _this.dHistoryParams = store.getters.dHistoryParams
import keyCodeOptions from './methods/keyCodeOptions'
import dealWithCtrl from './methods/dealWithCtrl'
import { useStore, Store } from 'vuex'
const ignoreNode = ['INPUT', 'TEXTAREA']
@ -36,94 +37,98 @@ let hadDown = false
const shortcuts = {
methods: {
handleKeydowm(e: any) {
const nodeName = e.target.nodeName
if (ignoreNode.indexOf(nodeName) !== -1 || (nodeName === 'DIV' && e.target.contentEditable === 'true')) {
return
}
// if (hadDown || this.showMenuBg) {
// e.stopPropagation()
// e.preventDefault()
// return
// }
// hadDown = true
const ctrl = e.key === 'Control' || e.key === 'Meta'
const alt = e.key === 'Alt'
const shift = e.key === 'Shift'
// const dir = e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40
// const specialKey = ctrl || alt || shift || dir
// if (specialKey || e.metaKey) {
// hadDown = false
// }
if (shift || ctrl) {
this.$store.dispatch('updateAltDown', true)
clearInterval(this.checkCtrl)
this.checkCtrl = setInterval(() => {
// TODO: 防止组合键导致页面失焦无法操作
if (!document.hasFocus()) {
clearInterval(this.checkCtrl)
hadDown = false
this.$store.dispatch('updateAltDown', false)
}
}, 500)
}
// const systemKey = systemKeyCode.find((item) => {
// let f = false
// let f2 = false
// for (let i = 0; i < item.key.length; ++i) {
// f = e[item.key[i]]
// if (f) {
// break
// }
// }
// if (item.key2) {
// for (let i = 0; i < item.key2.length; ++i) {
// f2 = e[item.key2[i]]
// if (f2) {
// break
// }
// }
// }
// return f && f2 && e.keyCode === item.code
// })
// if (systemKey) {
// return
// }
const withCtrl = e.ctrlKey || e.metaKey
if (withCtrl && !(ctrl || alt || shift)) {
this.dealCtrl(e)
return
}
// const withAlt = e.altKey
// if (withAlt && !specialKey) {
// return
// }
const withShift = e.shiftKey
// if (withShift && !specialKey) {
// return
// }
// // TODO
// if (!this.dActiveElement) {
// return
// }
// if (this.dActiveElement.uuid === '-1') {
// return
// }
// e.stopPropagation()
// e.preventDefault()
const f = withShift ? 10 : 1
keyCodeOptions(e, { f })
},
handleKeyup(e) {
console.log(e)
clearInterval(this.checkCtrl)
hadDown = false
if (e.key === 'Alt' || e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') {
this.$store.dispatch('updateAltDown', false)
handleKeydowm(store: Store<any>, checkCtrl: number | undefined, instance: any, dealCtrl: (e: any, instance: any) => void) {
return (e: any) => {
const nodeName = e.target.nodeName
if (ignoreNode.indexOf(nodeName) !== -1 || (nodeName === 'DIV' && e.target.contentEditable === 'true')) {
return
}
// if (hadDown || this.showMenuBg) {
// e.stopPropagation()
// e.preventDefault()
// return
// }
// hadDown = true
const ctrl = e.key === 'Control' || e.key === 'Meta'
const alt = e.key === 'Alt'
const shift = e.key === 'Shift'
// const dir = e.keyCode === 37 || e.keyCode === 38 || e.keyCode === 39 || e.keyCode === 40
// const specialKey = ctrl || alt || shift || dir
// if (specialKey || e.metaKey) {
// hadDown = false
// }
if (shift || ctrl) {
store.dispatch('updateAltDown', true)
clearInterval(checkCtrl)
checkCtrl = setInterval(() => {
// TODO: 防止组合键导致页面失焦无法操作
if (!document.hasFocus()) {
clearInterval(checkCtrl)
hadDown = false
store.dispatch('updateAltDown', false)
}
}, 500)
}
// const systemKey = systemKeyCode.find((item) => {
// let f = false
// let f2 = false
// for (let i = 0; i < item.key.length; ++i) {
// f = e[item.key[i]]
// if (f) {
// break
// }
// }
// if (item.key2) {
// for (let i = 0; i < item.key2.length; ++i) {
// f2 = e[item.key2[i]]
// if (f2) {
// break
// }
// }
// }
// return f && f2 && e.keyCode === item.code
// })
// if (systemKey) {
// return
// }
const withCtrl = e.ctrlKey || e.metaKey
if (withCtrl && !(ctrl || alt || shift)) {
dealCtrl(e, instance)
return
}
// const withAlt = e.altKey
// if (withAlt && !specialKey) {
// return
// }
const withShift = e.shiftKey
// if (withShift && !specialKey) {
// return
// }
// // TODO
// if (!this.dActiveElement) {
// return
// }
// if (this.dActiveElement.uuid === '-1') {
// return
// }
// e.stopPropagation()
// e.preventDefault()
const f = withShift ? 10 : 1
keyCodeOptions(e, { f })
}
},
dealCtrl(e: any) {
dealWithCtrl(e, this)
handleKeyup(store: Store<any>, checkCtrl: number | undefined) {
return (e: any) => {
console.log(e)
clearInterval(checkCtrl)
hadDown = false
if (e.key === 'Alt' || e.key === 'Shift' || e.key === 'Control' || e.key === 'Meta') {
store.dispatch('updateAltDown', false)
}
}
},
dealCtrl(e: any, instance: any) {
dealWithCtrl(e, instance)
console.log(e.key, e.keyCode)
},
},

View File

@ -1,3 +1,5 @@
import { RouteRecordRaw } from 'vue-router';
/*
* @Author: ShawnPhang
* @Date: 2021-08-19 18:43:22
@ -40,4 +42,4 @@ export default [
name: 'Psd',
component: () => import(/* webpackChunkName: 'psd' */ '@/views/Psd.vue'),
},
]
] as RouteRecordRaw[]

View File

@ -1,8 +1,10 @@
// import store from '@/store'
export default (router: Type.Object) => {
import { NavigationGuardNext, RouteLocationNormalized, Router } from "vue-router"
export default (router: Router) => {
router.beforeEach((to: Type.Object, from: Type.Object, next: () => void) => {
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
// if (to.meta.requireAuth) { }
// 有必要时清除残余的loading框

11
src/types/env.d.ts vendored
View File

@ -1,4 +1,4 @@
/// <reference lib="dom" />
// / <reference lib="dom" />
interface ImportMeta {
url: string
@ -23,13 +23,14 @@ interface ImportMeta {
on(event: string, cb: (...args: any[]) => void): void
}
readonly env: ImportMetaEnv
// readonly env: ImportMetaEnv
glob(pattern: string): Record<
glob(pattern: string, { eager: boolean }): Record<
string,
() => Promise<{
{
[key: string]: any
}>
}
>
globEager(pattern: string): Record<

65
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,65 @@
/** 公共API返回结果 */
type TCommResResult<T> = {
code: number
msg: string
result: T
}
type TCommonItemData = {
type: string
fontFamily?: string
color?: string
fontSize: number
width: number
height: number
left: number
top: number
fontWeight: number
value: TItem2DataParam
}
/** 分页查询公共返回 */
type TPageRequestResult<T> = {
list: T
total: number
}
interface HTMLElementEventMap {
"mousewheel": MouseEvent
}
interface IQiniuSubscribeCb {
(result: {
total: { percent: number }
key: string
hash: string
}): void
}
interface Window {
qiniu: {
upload: (
file: File | Blob,
name: string,
token: string,
exObj: Record<string, any>,
exOption: {
useCdnDomain: boolean
}) => {
subscribe: (cb: {
next: IQiniuSubscribeCb
error: (err: string) => void
complete: IQiniuSubscribeCb
}) => void
}
}
}
interface MouseEvent {
layerX: number
layerY: number
}

View File

@ -2,12 +2,12 @@
* @Author: ShawnPhang
* @Date: 2021-07-13 02:48:38
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2024-01-11 17:36:33
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-02-26 17:54:00
*/
import axios from 'axios'
import axios, { AxiosRequestConfig, AxiosResponse, AxiosStatic } from 'axios'
import store from '@/store'
import app_config from '@/config'
import app_config, { LocalStorageKey } from '@/config'
axios.defaults.timeout = 30000
axios.defaults.headers.authorization = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAwMDEsImV4cCI6MTc4ODU3NDc1MDU4NX0.L_t6DFD48Dm6rUPfgIgOWJkz18En1m_-hhMHcpbxliY';
@ -16,15 +16,15 @@ const baseUrl = app_config.API_URL
// 请求拦截器
axios.interceptors.request.use(
(config: Type.Object) => {
(config: AxiosRequestConfig) => {
// const access_token = store.state.currentUser.access_token;
const url = config.url
const url = config.url ?? ""
const values = {}
// values.access_token = access_token;
// values.version = version;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url.indexOf('/') === 0 ? (config.url = baseUrl + url) : (config.url = baseUrl + '/' + url)
if (!url.startsWith('http://') && !url.startsWith('https://')) {
config.url = url.startsWith('/') ? baseUrl + url : config.url = baseUrl + '/' + url
}
if (config.method === 'get') {
@ -44,10 +44,8 @@ axios.interceptors.request.use(
)
// 响应拦截器
axios.interceptors.response.use(
(res: Type.Object) => {
axios.interceptors.response.use((res: AxiosResponse<any>) => {
// store.dispatch('hideLoading');
// 接口规则只有正确code为200时返回result结果对象错误返回整个结果对象
if (!res.data) {
@ -74,16 +72,28 @@ axios.interceptors.response.use(
},
)
type TFetchRequestConfigParams = AxiosRequestConfig & Record<string, any>
type TFetchMethod = keyof Pick<
AxiosStatic,
"get" | "post" | "put" | "getUri" | "request" | "delete" | "head" | "options" | "patch"
>
// export default axios;
const fetch = (url: string, params: Type.Object, type: string | undefined = 'get', exheaders: Type.Object = {}, extra: any = {}) => {
if (params && params._noLoading) {
const fetch = <T = any> (
url: string,
params: TFetchRequestConfigParams,
type: TFetchMethod = 'get',
exheaders: Record<string, any> = {},
extra: Record<string, any> = {}
): Promise<T> => {
if (params?._noLoading) {
delete params._noLoading
} else {
// store.commit('loading', '加载中..');
}
const token = localStorage.getItem('xp_token')
const headerObject: Type.Object = { }
const token = localStorage.getItem(LocalStorageKey.tokenKey)
const headerObject: Record<string, any> = {}
token && (headerObject.authorization = token)
if (type === 'get') {
@ -93,10 +103,10 @@ const fetch = (url: string, params: Type.Object, type: string | undefined = 'get
...extra,
})
} else {
return (axios as Type.Object)[type](url, params, {
return axios[type](url, params, {
headers: Object.assign(headerObject, exheaders),
...extra,
})
}) as Promise<T>
}
}

View File

@ -2,7 +2,7 @@
* @Author: ShawnPhang
* @Date: 2021-07-13 02:48:38
* @Description:
* @LastEditors: ShawnPhang
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2022-03-07 20:25:54
*/
// import store from '../store'
@ -11,12 +11,13 @@ import * as utils from './utils'
import _config from '@/config'
import modules from './plugins/modules'
import cssLoader from './plugins/cssLoader'
import type {App} from 'vue'
/**
*
*/
export default {
install(myVue: Type.Object) {
install(myVue: App) {
/** 全局组件注册 */
modules(myVue)
/** iconfont 注入 */

View File

@ -7,8 +7,9 @@
*/
// import { Button, Field, Divider, NavBar, Toast, Popup } from 'vant'
import coms from '@/components/modules'
import { App } from 'vue'
export default (Vue: any) => {
export default (Vue: App) => {
coms(Vue)
// Vue.component(Button.name, Button)
// Vue.use(Field).use(Divider).use(NavBar).use(Toast).use(Popup)

View File

@ -6,9 +6,10 @@
* @LastEditTime: 2023-09-19 17:32:40
*/
export default class PointImg {
private canvas: any
private cvs: any
constructor(img: any) {
private canvas: HTMLCanvasElement | undefined
private cvs: CanvasRenderingContext2D | null | undefined
constructor(img: HTMLImageElement) {
if (img.src) {
try {
this.canvas = document.createElement('canvas')
@ -16,6 +17,8 @@ export default class PointImg {
this.canvas.height = img.height
img.crossOrigin = 'Anonymous'
this.cvs = this.canvas.getContext('2d')
if (!this.cvs) return
this.cvs.drawImage(img, 0, 0, img.width, img.height)
} catch (error) {
console.log(error)
@ -28,25 +31,27 @@ export default class PointImg {
* @param y Number y坐标起点
* @return color Object rgba #16
*/
const color: any = {}
const color: Record<string, string> = {}
try {
const obj = this.cvs.getImageData(x, y, 1, 1)
const arr = obj.data.toString().split(',')
if (this.cvs) {
const obj = this.cvs.getImageData(x, y, 1, 1)
const arr = obj.data.toString().split(',')
let first = parseInt(arr[0], 10).toString(16)
first = first.length === 2 ? first : first + first
let first = parseInt(arr[0], 10).toString(16)
first = first.length === 2 ? first : first + first
let second = parseInt(arr[1], 10).toString(16)
second = second.length === 2 ? second : second + second
let second = parseInt(arr[1], 10).toString(16)
second = second.length === 2 ? second : second + second
let third = parseInt(arr[2], 10).toString(16)
third = third.length === 2 ? third : third + third
let third = parseInt(arr[2], 10).toString(16)
third = third.length === 2 ? third : third + third
let last = parseInt(arr.pop(), 10) / 255
last = Number(last.toFixed(0))
let last = parseInt(arr.pop() || '0', 10) / 255
last = Number(last.toFixed(0))
color['rgba'] = 'rgba(' + arr.join(',') + ',' + last + ')'
color['#'] = '#' + first + second + third
color['rgba'] = 'rgba(' + arr.join(',') + ',' + last + ')'
color['#'] = '#' + first + second + third
}
} catch (error) {
// console.log('此为解析图片点位异常')
}

View File

@ -2,27 +2,27 @@
* @Author: ShawnPhang
* @Date: 2021-12-24 15:13:58
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-19 17:19:07
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-05 12:00:00
*/
export default class PreLoad {
private i: number
private arr: any[]
constructor(arr: string[]) {
private arr: (string | HTMLImageElement | ChildNode[])[]
constructor(arr: (string | HTMLImageElement | ChildNode[])[]) {
this.i = 0
this.arr = arr
}
public imgs() {
return new Promise((resolve: any) => {
return new Promise<void>((resolve) => {
const work = (src: string) => {
if (this.i < this.arr.length) {
const img = new Image()
img.src = src
if (img.complete) {
work(this.arr[this.i++])
work(this.arr[this.i++] as string)
} else {
img.onload = () => {
work(this.arr[this.i++])
work(this.arr[this.i++] as string)
img.onload = null
}
}
@ -31,14 +31,14 @@ export default class PreLoad {
resolve()
}
}
work(this.arr[this.i])
work(this.arr[this.i] as string)
})
}
public doms() {
return new Promise((resolve: Function) => {
return new Promise<void>((resolve) => {
const work = () => {
if (this.i < this.arr.length) {
this.arr[this.i].complete && this.i++
(this.arr[this.i] as HTMLImageElement).complete && this.i++
setTimeout(() => {
work()
}, 100)
@ -51,10 +51,10 @@ export default class PreLoad {
}
/** 判断是否加载svg */
public svgs() {
return new Promise((resolve: Function) => {
return new Promise<void>((resolve) => {
const work = () => {
if (this.i < this.arr.length) {
this.arr[this.i].length > 0 && this.i++
(this.arr[this.i] as ChildNode[]).length > 0 && this.i++
setTimeout(() => {
work()
}, 100)

View File

@ -2,11 +2,11 @@
* @Author: ShawnPhang
* @Date: 2022-03-06 13:53:30
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-09-14 17:28:53
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-03-05 12:00:00
*/
export default class WebWorker {
private worker: any
private worker: Worker | undefined
constructor(name: string) {
if (typeof Worker === 'undefined') {
@ -21,12 +21,15 @@ export default class WebWorker {
}
public start(data: any) {
return new Promise((resolve) => {
// 监听Web Worker的消息
this.worker.onmessage = (e: any) => {
resolve(e.data)
if (!this.worker) resolve('')
else {
// 监听Web Worker的消息
this.worker.onmessage = (e) => {
resolve(e.data)
}
// 发送数据给Web Worker
this.worker.postMessage(data)
}
// 发送数据给Web Worker
this.worker.postMessage(data)
})
}
}

View File

@ -1,6 +1,8 @@
import app_config from '@/config'
export const config = app_config
type TComObj = Record<string,any>
/**
*
* @param {String} 'YYYY-MM-DD'
@ -38,30 +40,31 @@ export const config = app_config
// }
// }
// 判断是否在数组中并返回下标
export const isInArray = (arr: Type.Object[], value: any) => {
if (arr.indexOf && typeof arr.indexOf === 'function') {
const index = arr.indexOf(value)
if (index >= 0) {
return index
}
export const isInArray = (arr: (string | number)[], value: (string | number)) => {
const index = arr.indexOf(value)
if (index >= 0) {
return index
}
return false
}
/** 删除多个对象元素 */
export const deleteSome = (obj: Type.Object, arr: string[]) => {
export const deleteSome = <R extends TComObj, T extends TComObj = TComObj>(obj: T, arr: string[]) => {
arr.forEach((key) => {
delete obj[key]
})
return obj
return obj as R extends T ? R : Partial<T>
}
/** 拾取对象元素 */
export const pickSome = (obj: Type.Object, arr: string[]) => {
const newObj: Type.Object = {}
export const pickSome = <R extends TComObj, T extends TComObj = TComObj>(obj: T, arr: string[]) => {
const newObj: Record<string, any> = {}
arr.forEach((key) => {
newObj[key] = obj[key]
})
return newObj
return newObj as R extends T ? R : Partial<T>
}
/** String长度 */
// export const getBLen = (str: string | any) => {
// if (str === null) {

View File

@ -169,7 +169,7 @@ const components = [
// ElUpload,
]
const plugins: any = [
const plugins = [
ElInfiniteScroll,
ElLoading,
// ElMessage,

View File

@ -2,8 +2,8 @@
* @Author: ShawnPhang
* @Date: 2023-08-23 17:37:16
* @Description:
* @LastEditors: ShawnPhang <https://m.palxp.cn>
* @LastEditTime: 2023-10-14 18:31:29
* @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastEditTime: 2024-02-27 10:32:00
*/
/**
* ttf/otf这种原始字体支持提取false

View File

@ -8,132 +8,140 @@
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { mapActions, mapGetters } from 'vuex'
<script lang="ts" setup>
import { StyleValue, onMounted, reactive, nextTick } from 'vue'
import { useStore } from 'vuex'
import api from '@/api'
import wGroup from '@/components/modules/widgets/wGroup/wGroup.vue'
import Preload from '@/utils/plugins/preload'
import FontFaceObserver from 'fontfaceobserver'
import { fontWithDraw, font2style } from '@/utils/widgets/loadFontRule'
import designBoard from '@/components/modules/layout/designBoard.vue'
import zoomControl from '@/components/modules/layout/zoomControl.vue'
import designBoard from '@/components/modules/layout/designBoard/index.vue'
import zoomControl from '@/components/modules/layout/zoomControl/index.vue'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
import { useRoute } from 'vue-router'
import { wGroupSetting } from '@/components/modules/widgets/wGroup/groupSetting'
export default defineComponent({
components: { designBoard, zoomControl },
// mixins: [shortcuts],
setup() {
const state = reactive({
style: {
left: '0px',
},
})
type TState = {
style: StyleValue
}
return {
...toRefs(state),
}
},
computed: {
...mapGetters(['dPage']),
},
mounted() {
this.initGroupJson(JSON.stringify(wGroup.setting))
this.$nextTick(() => {
this.load()
})
},
methods: {
...mapActions(['initGroupJson', 'setTemplate', 'addGroup']),
async load() {
let loadFlag = false
const { id, tempid, tempType: type } = this.$route.query
if (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)
}
await this.$nextTick()
const imgsData: any = []
const svgsData: any = []
const fontLoaders: any = []
const fontContent: any = {}
let fontData: any = []
widgets.forEach((item: any) => {
if (item.fontClass && item.fontClass.value) {
const loader = new FontFaceObserver(item.fontClass.value)
fontData.push(item.fontClass)
fontLoaders.push(loader.load(null, 30000)) //
//
if (fontContent[item.fontClass.value]) {
fontContent[item.fontClass.value] += item.text
} else {
fontContent[item.fontClass.value] = item.text
}
}
// svg
try {
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)
}
}
}
} catch (e) {}
})
// TODO:
if (content.page?.backgroundImage) {
const preloadBg = new Preload([content.page.backgroundImage])
await preloadBg.imgs()
}
try {
fontWithDraw && (await font2style(fontContent, fontData))
// console.log('1. base64 yes')
const preload = new Preload(imgsData)
await preload.doms()
// console.log('2. image yes')
const preload2 = new Preload(svgsData)
await preload2.svgs()
// console.log('3. svg yes')
} catch (e) {
console.log(e)
}
try {
await Promise.all(fontLoaders)
// console.log('4. font yes')
} catch (e) {
// console.log(e)
}
loadFlag = true
console.log('--> now u can start screenshot!')
setTimeout(() => {
try {
;(window as any).loadFinishToInject('done')
} catch (err) {}
}, 100)
}
//
setTimeout(() => {
!loadFlag && (window as any).loadFinishToInject('done')
}, 60000)
},
// mixins: [shortcuts],
const store = useStore()
const route = useRoute()
const state = reactive<TState>({
style: {
left: '0px',
},
})
const { dPage } = useSetupMapGetters(['dPage'])
onMounted(() => {
store.dispatch('initGroupJson', JSON.stringify(wGroupSetting))
// initGroupJson(JSON.stringify(wGroup.setting))
nextTick(() => {
load()
})
})
// ...mapActions(['initGroupJson', 'setTemplate', 'addGroup']),
async function load() {
let loadFlag = false
const { id, tempid, tempType: type } = route.query
if (id || tempid) {
const postData = {
id: Number(id || tempid),
type: Number(type)
}
const { data, width, height } = await api.home[id ? 'getWorks' : 'getTempDetail'](postData)
const content = JSON.parse(data)
const widgets = Number(type) == 1 ? content : content.widgets
if (Number(type) == 1) {
dPage.value.width = width
dPage.value.height = height
dPage.value.backgroundColor = '#ffffff00'
store.dispatch('addGroup', content)
// addGroup(content)
} else {
store.commit('setDPage', content.page)
if (id) {
store.commit('setDWidgets', widgets)
} else {
store.dispatch('setTemplate', widgets)
}
}
await nextTick()
const imgsData: HTMLImageElement[] = []
const svgsData: HTMLImageElement[] = []
const fontLoaders: Promise<void>[] = []
const fontContent: Record<string, string> = {}
let fontData: string[] = []
widgets.forEach((item: any) => {
if (item.fontClass && item.fontClass.value) {
const loader = new FontFaceObserver(item.fontClass.value)
fontData.push(item.fontClass)
fontLoaders.push(loader.load(null, 30000)) //
//
if (fontContent[item.fontClass.value]) {
fontContent[item.fontClass.value] += item.text
} else {
fontContent[item.fontClass.value] = item.text
}
}
// svg
try {
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)
}
}
}
} catch (e) {}
})
// TODO:
if (content.page?.backgroundImage) {
const preloadBg = new Preload([content.page.backgroundImage])
await preloadBg.imgs()
}
try {
fontWithDraw && (await font2style(fontContent, fontData))
// console.log('1. base64 yes')
const preload = new Preload(imgsData)
await preload.doms()
// console.log('2. image yes')
const preload2 = new Preload(svgsData)
await preload2.svgs()
// console.log('3. svg yes')
} catch (e) {
console.log(e)
}
try {
await Promise.all(fontLoaders)
// console.log('4. font yes')
} catch (e) {
// console.log(e)
}
loadFlag = true
console.log('--> now u can start screenshot!')
setTimeout(() => {
try {
;(window as any).loadFinishToInject('done')
} catch (err) {}
}, 100)
}
//
setTimeout(() => {
!loadFlag && (window as any).loadFinishToInject('done')
}, 60000)
}
</script>
<style lang="less" scoped>

View File

@ -1,9 +1,17 @@
<!--
* @Author: ShawnPhang
* @Date: 2023-09-18 17:34:44
* @Description:
* @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
* @LastUpdateContent: Support typescript
* @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">
<div :style="state.style" class="top-nav">
<div class="top-nav-wrap">
<div class="top-left">
<div class="name" @click="jump2home">{{ APP_NAME }}</div>
<div class="name" @click="jump2home">{{ state.APP_NAME }}</div>
<div class="operation">
<div :class="['operation-item', { disable: !undoable }]" @click="undoable ? handleHistory('undo') : ''"><i class="iconfont icon-undo" /></div>
<div :class="['operation-item', { disable: !redoable }]" @click="redoable ? handleHistory('redo') : ''"><i class="iconfont icon-redo" /></div>
@ -12,7 +20,7 @@
<i style="font-size: 20px" class="icon sd-biaochi extra-operation" @click="changeLineGuides" />
</el-tooltip>
</div>
<HeaderOptions ref="options" v-model="isContinue" @change="optionsChange" />
<HeaderOptions ref="optionsRef" v-model="state.isContinue" @change="optionsChange" />
</div>
</div>
<div class="page-design-index-wrap">
@ -26,150 +34,196 @@
<style-panel></style-panel>
</div>
<!-- 标尺 -->
<line-guides :show="showLineGuides" />
<line-guides :show="state.showLineGuides" />
<!-- 缩放控制 -->
<zoom-control ref="zoomControl" />
<zoom-control ref="zoomControlRef" />
<!-- 右键菜单 -->
<right-click-menu />
<!-- 旋转缩放组件 -->
<Moveable />
<!-- 遮罩百分比进度条 -->
<ProgressLoading :percent="downloadPercent" :text="downloadText" cancelText="取消" @cancel="downloadCancel" @done="downloadPercent = 0" />
<ProgressLoading
:percent="state.downloadPercent"
:text="state.downloadText"
cancelText="取消"
@cancel="downloadCancel"
@done="state.downloadPercent = 0"
/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import _config from '../config'
import { defineComponent, reactive, toRefs } from 'vue'
import { mapActions, mapGetters } from 'vuex'
import {
CSSProperties, computed, nextTick,
onBeforeUnmount, onMounted, reactive, ref,
getCurrentInstance
} from 'vue'
import { useStore } from 'vuex'
import RightClickMenu from '@/components/business/right-click-menu/RcMenu.vue'
import Moveable from '@/components/business/moveable/Moveable.vue'
import designBoard from '@/components/modules/layout/designBoard.vue'
import zoomControl from '@/components/modules/layout/zoomControl.vue'
import designBoard from '@/components/modules/layout/designBoard/index.vue'
import zoomControl from '@/components/modules/layout/zoomControl/index.vue'
import lineGuides from '@/components/modules/layout/lineGuides.vue'
import shortcuts from '@/mixins/shortcuts'
import wGroup from '@/components/modules/widgets/wGroup/wGroup.vue'
import HeaderOptions from './components/HeaderOptions.vue'
import ProgressLoading from '@/components/common/ProgressLoading/index.vue'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
import { useRoute } from 'vue-router'
import { wGroupSetting } from '@/components/modules/widgets/wGroup/groupSetting'
const beforeUnload = function (e: any) {
const confirmationMessage = '系统不会自动保存您未修改的内容'
;(e || window.event).returnValue = confirmationMessage // Gecko and Trident
type TState = {
style: CSSProperties
downloadPercent: number //
downloadText: string
isContinue: boolean
APP_NAME: string
showLineGuides: boolean
}
const beforeUnload = function (e: Event): string {
const confirmationMessage: string = '系统不会自动保存您未修改的内容';
(e || window.event).returnValue = (confirmationMessage as any) // Gecko and Trident
return confirmationMessage // Gecko and WebKit
}
export default defineComponent({
components: {
RightClickMenu,
Moveable,
HeaderOptions,
ProgressLoading,
designBoard,
zoomControl,
lineGuides,
},
mixins: [shortcuts],
setup() {
!_config.isDev && window.addEventListener('beforeunload', beforeUnload)
// mixins: [shortcuts],
!_config.isDev && window.addEventListener('beforeunload', beforeUnload)
const state = reactive({
style: {
left: '0px',
},
// openDraw: false,
downloadPercent: 0, //
downloadText: '',
isContinue: true,
APP_NAME: _config.APP_NAME,
showLineGuides: false,
})
// const draw = () => {
// state.openDraw = true
// }
function jump2home() {
// const fullPath = window.location.href.split('/')
// window.open(fullPath[0] + '//' + fullPath[2])
window.open('https://xp.palxp.cn/')
}
return {
...toRefs(state),
jump2home,
}
},
computed: {
...mapGetters(['dActiveElement', 'dHistoryParams', 'dCopyElement', 'dPage', 'dZoom']),
undoable() {
return !(this.dHistoryParams.index === -1 || (this.dHistoryParams === 0 && this.dHistoryParams.length === this.dHistoryParams.maxLength))
},
redoable() {
return !(this.dHistoryParams.index === this.dHistoryParams.length - 1)
},
},
// watch: {
// $route() {
// console.log('change route', this.$route.query)
// this.loadData()
// },
// },
mounted() {
this.initGroupJson(JSON.stringify(wGroup.setting))
window.addEventListener('scroll', this.fixTopBarScroll)
// window.addEventListener('click', this.clickListener)
document.addEventListener('keydown', this.handleKeydowm, false)
document.addEventListener('keyup', this.handleKeyup, false)
this.loadData()
},
beforeUnmount() {
window.removeEventListener('scroll', this.fixTopBarScroll)
// window.removeEventListener('click', this.clickListener)
document.removeEventListener('keydown', this.handleKeydowm, false)
document.removeEventListener('keyup', this.handleKeyup, false)
document.oncontextmenu = null
},
methods: {
...mapActions(['selectWidget', 'initGroupJson', 'handleHistory']),
changeLineGuides() {
this.showLineGuides = !this.showLineGuides
},
downloadCancel() {
this.downloadPercent = 0
this.isContinue = false
},
loadData() {
//
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
this.selectWidget({
uuid: '-1',
})
})
},
zoomSub() {
;(this.$refs as any).zoomControl.sub()
},
zoomAdd() {
;(this.$refs as any).zoomControl.add()
},
save() {
;(this.$refs as any).options.save()
},
fixTopBarScroll() {
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
this.style.left = `-${scrollLeft}px`
},
clickListener(e) {
console.log('click listener', e)
},
optionsChange({ downloadPercent, downloadText }: any) {
this.downloadPercent = downloadPercent
this.downloadText = downloadText
},
const {
dActiveElement, dHistoryParams, dCopyElement, dPage, dZoom
} = useSetupMapGetters(['dActiveElement', 'dHistoryParams', 'dCopyElement', 'dPage', 'dZoom'])
const state = reactive<TState>({
style: {
left: '0px',
},
// openDraw: false,
downloadPercent: 0, //
downloadText: '',
isContinue: true,
APP_NAME: _config.APP_NAME,
showLineGuides: false,
})
const optionsRef = ref<typeof HeaderOptions | null>(null)
const zoomControlRef = ref<typeof zoomControl | null>(null)
const store = useStore()
const route = useRoute()
// const draw = () => {
// state.openDraw = true
// }
function jump2home() {
// const fullPath = window.location.href.split('/')
// window.open(fullPath[0] + '//' + fullPath[2])
window.open('https://xp.palxp.cn/')
}
defineExpose({
jump2home,
})
const undoable = computed(() => {
return !(
dHistoryParams.value.index === -1 ||
(dHistoryParams.value === 0 && dHistoryParams.value.length === dHistoryParams.value.maxLength))
})
const redoable = computed(() => {
return !(dHistoryParams.value.index === dHistoryParams.value.length - 1)
})
// watch: {
// $route() {
// console.log('change route', this.$route.query)
// this.loadData()
// },
// },
const { handleKeydowm, handleKeyup, dealCtrl } = shortcuts.methods
let checkCtrl: number | undefined
onMounted(() => {
store.dispatch('initGroupJson', JSON.stringify(wGroupSetting))
// initGroupJson(JSON.stringify(wGroup.setting))
window.addEventListener('scroll', fixTopBarScroll)
// window.addEventListener('click', this.clickListener)
const instance = getCurrentInstance()
document.addEventListener('keydown', handleKeydowm(store, checkCtrl, instance, dealCtrl), false)
document.addEventListener('keyup', handleKeyup(store, checkCtrl), false)
loadData()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', fixTopBarScroll)
const instance = getCurrentInstance()
// window.removeEventListener('click', this.clickListener)
document.removeEventListener('keydown', handleKeydowm(store, checkCtrl, instance, dealCtrl), false)
document.removeEventListener('keyup', handleKeyup(store, checkCtrl), false)
document.oncontextmenu = null
})
// ...mapActions(['selectWidget', 'initGroupJson', 'handleHistory']),
function handleHistory(data: string) {
store.dispatch('handleHistory', data)
}
function changeLineGuides() {
state.showLineGuides = !state.showLineGuides
}
function downloadCancel() {
state.downloadPercent = 0
state.isContinue = false
}
function loadData() {
//
const { id, tempid, tempType } = route.query
if (!optionsRef.value) return
optionsRef.value.load(id, tempid, tempType, async () => {
if (!zoomControlRef.value) return
zoomControlRef.value.screenChange()
await nextTick()
// page
store.dispatch('selectWidget', { uuid: '-1' })
// selectWidget({
// uuid: '-1',
// })
})
}
function zoomSub() {
if (!zoomControlRef.value) return
zoomControlRef.value.sub()
}
function zoomAdd() {
if (!zoomControlRef.value) return
zoomControlRef.value.add()
}
function save() {
if (!optionsRef.value) return
optionsRef.value.save()
}
function fixTopBarScroll() {
const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
state.style.left = `-${scrollLeft}px`
}
function clickListener(e: Event) {
console.log('click listener', e)
}
function optionsChange({ downloadPercent, downloadText }: { downloadPercent: number, downloadText: string }) {
state.downloadPercent = downloadPercent
state.downloadText = downloadText
}
</script>
<style lang="less" scoped>

View File

@ -12,10 +12,12 @@
<div class="top-left">
<div class="name" style="font-size: 15px">在线PSD解析</div>
</div>
<div style="flex: 1"><el-button plain type="primary" @click="jump2word">说明文档及PSD规范</el-button></div>
<el-button v-show="isDone" @click="clear">清空模板</el-button>
<div style="flex: 1">
<el-button plain type="primary" @click="jump2word">说明文档及PSD规范</el-button>
</div>
<el-button v-show="state.isDone" @click="clear">清空模板</el-button>
<div class="v-tips">
<HeaderOptions :isDone="isDone" @change="optionsChange" />
<HeaderOptions :isDone="state.isDone" @change="optionsChange" />
</div>
</div>
</div>
@ -23,28 +25,38 @@
<div class="page-design-index-wrap">
<!-- <widget-panel></widget-panel> -->
<design-board class="page-design-wrap" pageDesignCanvasId="page-design-canvas">
<div v-if="isDone" class="shelter" :style="{ width: (dPage.width * dZoom) / 100 + 'px', height: (dPage.height * dZoom) / 100 + 'px' }"></div>
<div v-if="state.isDone" class="shelter" :style="{ width: (dPage.width * dZoom) / 100 + 'px', height: (dPage.height * dZoom) / 100 + 'px' }"></div>
<uploader v-else accept=".psd" :hold="true" :drag="true" class="uploader" @load="selectFile">
<div class="uploader__box"><img style="margin-right: 1rem" src="https://cdn.dancf.com/design/svg/icon_psdimport.37e6f23e.svg" /> 在此拖入或选择PSD文件</div>
<div class="uploader__box">
<img
style="margin-right: 1rem"
src="https://cdn.dancf.com/design/svg/icon_psdimport.37e6f23e.svg"
alt="upload"
/> PSD
</div>
</uploader>
</design-board>
<style-panel v-show="isDone"></style-panel>
<style-panel v-show="state.isDone"></style-panel>
</div>
<!-- 缩放控制 -->
<zoom-control v-if="isDone" ref="zoomControl" />
<zoom-control v-if="state.isDone" ref="zoomControlRef" />
<!-- 右键菜单 -->
<right-click-menu />
<!-- 旋转缩放组件 -->
<Moveable />
<!-- 遮罩百分比进度条 -->
<ProgressLoading :percent="downloadPercent" :text="downloadText" :cancelText="cancelText" :msg="downloadMsg" @cancel="cancel" @done="downloadPercent = 0" />
<ProgressLoading
:percent="state.downloadPercent" :text="state.downloadText"
:cancelText="state.cancelText" :msg="state.downloadMsg"
@cancel="cancel" @done="state.downloadPercent = 0"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, getCurrentInstance, ComponentInternalInstance, onMounted, nextTick } from 'vue'
<script lang="ts" setup>
import { reactive, onMounted, nextTick, onBeforeMount, ref, getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
import { mapActions, mapGetters, useStore } from 'vuex'
import { useStore } from 'vuex'
import RightClickMenu from '@/components/business/right-click-menu/RcMenu.vue'
import Moveable from '@/components/business/moveable/Moveable.vue'
import shortcuts from '@/mixins/shortcuts'
@ -52,127 +64,147 @@ import wText from '@/components/modules/widgets/wText/wText.vue'
import wImage from '@/components/modules/widgets/wImage/wImage.vue'
import useLoading from '@/common/methods/loading'
import uploader from '@/components/common/Uploader/index.vue'
import designBoard from '@/components/modules/layout/designBoard.vue'
import zoomControl from '@/components/modules/layout/zoomControl.vue'
import HeaderOptions from './components/UploadTemplate.vue'
import designBoard from '@/components/modules/layout/designBoard/index.vue'
import zoomControl from '@/components/modules/layout/zoomControl/index.vue'
import HeaderOptions, { TEmitChangeData } from './components/UploadTemplate.vue'
import ProgressLoading from '@/components/common/ProgressLoading/index.vue'
// import MyWorker from '@/utils/plugins/webWorker'
import { processPSD2Page } from '@/utils/plugins/psd'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
export default defineComponent({
components: { RightClickMenu, Moveable, uploader, designBoard, zoomControl, HeaderOptions, ProgressLoading },
mixins: [shortcuts],
setup() {
const state = reactive({
isDone: true,
downloadPercent: 0, //
downloadText: '',
downloadMsg: '',
cancelText: '',
})
const store = useStore()
const route = useRoute()
const { proxy }: any = getCurrentInstance() as ComponentInternalInstance
let loading: any = null
type TState = {
isDone: boolean
downloadPercent: number //
downloadText: string
downloadMsg: string
cancelText: string
}
// mixins: [shortcuts],
const state = reactive<TState>({
isDone: true,
downloadPercent: 0, //
downloadText: '',
downloadMsg: '',
cancelText: '',
})
const store = useStore()
const route = useRoute()
const { dPage, dZoom } = useSetupMapGetters(['dPage', 'dZoom'])
const zoomControlRef = ref<typeof zoomControl | null>()
let loading: ReturnType<typeof useLoading> | null = null
// const myWorker = new MyWorker('loadPSD')
onMounted(async () => {
await nextTick()
state.isDone = false
})
onMounted(async () => {
await nextTick()
if (zoomControlRef.value){
zoomControlRef.value.screenChange()
}
state.isDone = false
})
function loadJS() {
const link_element = document.createElement('script')
link_element.setAttribute('src', '/psd.js')
document.head.appendChild(link_element)
function loadJS() {
const link_element = document.createElement('script')
link_element.setAttribute('src', '/psd.js')
document.head.appendChild(link_element)
}
async function selectFile(file: File) {
loading = useLoading()
await loadPSD(file)
loading.close()
state.isDone = true
}
async function loadPSD(file: File) {
// const { compositeBuffer, psdFile } = await myWorker.start(file)
const data = await processPSD2Page(file)
setTimeout(async () => {
const types: any = {
text: wText.setting,
image: wImage.setting,
}
async function selectFile(file: any) {
loading = useLoading()
await loadPSD(file)
loading.close()
state.isDone = true
}
async function loadPSD(file: any) {
// const { compositeBuffer, psdFile } = await myWorker.start(file)
const data = await processPSD2Page(file)
setTimeout(async () => {
const types: any = {
text: wText.setting,
image: wImage.setting,
}
for (let i = 0; i < data.clouds.length; i++) {
const x: any = data.clouds[i]
const rawData = JSON.parse(JSON.stringify(types[x.type])) || {}
delete x.type
x.src && (x.imgUrl = x.src) && delete x.src
store.dispatch('addWidget', Object.assign(rawData, x))
}
const { width, height, background: bg } = data
store.commit('setDPage', Object.assign(store.getters.dPage, { width, height, backgroundColor: bg.color, backgroundImage: bg.image }))
await proxy?.loadDone()
}, 10)
for (let i = 0; i < data.clouds.length; i++) {
const x: any = data.clouds[i]
const rawData = JSON.parse(JSON.stringify(types[x.type])) || {}
delete x.type
x.src && (x.imgUrl = x.src) && delete x.src
store.dispatch('addWidget', Object.assign(rawData, x))
}
async function clear() {
store.commit('setDWidgets', [])
store.commit('setDPage', Object.assign(store.getters.dPage, { width: 1920, height: 1080, backgroundColor: '#ffffff', backgroundImage: '' }))
store.commit('setShowMoveable', false)
await nextTick()
state.isDone = false
}
const { width, height, background: bg } = data
store.commit('setDPage', Object.assign(store.getters.dPage, { width, height, backgroundColor: bg.color, backgroundImage: bg.image }))
await loadDone()
}, 10)
}
const optionsChange = ({ downloadPercent, downloadText, downloadMsg = '', cancelText = '' }: any) => {
typeof downloadPercent === 'number' && (state.downloadPercent = downloadPercent)
state.downloadText = downloadText
state.downloadMsg = downloadMsg
state.cancelText = cancelText
}
const cancel = () => {
state.downloadPercent = 100
window.open(`${window.location.protocol + '//' + window.location.host}/home?id=${route.query.id}`)
}
async function clear() {
store.commit('setDWidgets', [])
store.commit('setDPage', Object.assign(store.getters.dPage, { width: 1920, height: 1080, backgroundColor: '#ffffff', backgroundImage: '' }))
store.commit('setShowMoveable', false)
await nextTick()
state.isDone = false
}
return {
...toRefs(state),
loadJS,
selectFile,
clear,
cancel,
optionsChange,
}
},
computed: {
...mapGetters(['dPage', 'dZoom']),
},
async mounted() {
document.addEventListener('keydown', this.handleKeydowm, false)
document.addEventListener('keyup', this.handleKeyup, false)
this.loadJS()
},
beforeUnmount() {
document.removeEventListener('keydown', this.handleKeydowm, false)
document.removeEventListener('keyup', this.handleKeyup, false)
document.oncontextmenu = null
},
methods: {
...mapActions(['selectWidget']),
async loadDone() {
await this.$nextTick()
;(this.$refs as any).zoomControl.screenChange()
setTimeout(() => {
this.selectWidget({
uuid: '-1',
})
// this.$store.commit('setShowMoveable', false)
}, 100)
},
jump2word() {
window.open('https://xp.palxp.cn/#/articles/1687855172725')
// window.open('https://kdocs.cn/l/clmBsIkhve8d')
},
},
const optionsChange = ({ downloadPercent, downloadText, downloadMsg = '', cancelText = '' }: TEmitChangeData) => {
typeof downloadPercent === 'number' && (state.downloadPercent = downloadPercent)
state.downloadText = downloadText
state.downloadMsg = downloadMsg
state.cancelText = cancelText
}
const cancel = () => {
state.downloadPercent = 100
window.open(`${window.location.protocol + '//' + window.location.host}/home?id=${route.query.id}`)
}
const {handleKeydowm, handleKeyup, dealCtrl} = shortcuts.methods
// ...mapGetters(['dPage', 'dZoom']),
let checkCtrl: number | undefined
onMounted(() => {
const instance = getCurrentInstance()
document.addEventListener('keydown', handleKeydowm(store, checkCtrl, instance, dealCtrl), false)
document.addEventListener('keyup', handleKeyup(store, checkCtrl), false)
loadJS()
})
onBeforeMount(() => {
const instance = getCurrentInstance()
document.removeEventListener('keydown', handleKeydowm(store, checkCtrl, instance, dealCtrl), false)
document.removeEventListener('keyup', handleKeyup(store, checkCtrl), false)
document.oncontextmenu = null
})
// ...mapActions(['selectWidget']),
async function loadDone() {
await nextTick()
if (!zoomControlRef.value) return
zoomControlRef.value.screenChange()
setTimeout(() => {
store.dispatch('selectWidget', { uuid: '-1' })
// selectWidget({
// uuid: '-1',
// })
// this.$store.commit('setShowMoveable', false)
}, 100)
}
function jump2word() {
window.open('https://xp.palxp.cn/#/articles/1687855172725')
// window.open('https://kdocs.cn/l/clmBsIkhve8d')
}
defineExpose({
loadJS,
selectFile,
clear,
cancel,
optionsChange,
})
</script>

View File

@ -13,19 +13,10 @@
</tool-tip>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script lang="ts" setup>
import toolTip from '@/components/common/PopoverTip.vue'
export default defineComponent({
components: { toolTip },
setup() {
const content = '本站为个人项目,所使用素材图片等均为网络收集而来,下载之作品仅供学习研究或欣赏目的而使用,无法提供商用授权哦。'
return {
content,
}
},
})
const content = '本站为个人项目,所使用素材图片等均为网络收集而来,下载之作品仅供学习研究或欣赏目的而使用,无法提供商用授权哦。'
</script>
<style lang="less" scoped>

View File

@ -6,10 +6,10 @@
* @LastEditTime: 2023-12-11 12:40:59
-->
<template>
<div class="top-title"><el-input v-model="title" placeholder="未命名的设计" class="input-wrap" /></div>
<div class="top-title"><el-input v-model="state.title" placeholder="未命名的设计" class="input-wrap" /></div>
<div class="top-icon-wrap">
<template v-if="tempEditing">
<span style="color: #999; font-size: 14px; margin-right: 0.5rem">{{ stateBollean ? '启用' : '停用' }}</span> <el-switch v-model="stateBollean" @change="stateChange" />
<span style="color: #999; font-size: 14px; margin-right: 0.5rem">{{ state.stateBollean ? '启用' : '停用' }}</span> <el-switch v-model="state.stateBollean" @change="stateChange" />
<div class="divide__line">|</div>
<el-button plain type="primary" @click="saveTemp">保存模板</el-button>
<el-button @click="$store.commit('managerEdit', false)">取消</el-button>
@ -18,16 +18,16 @@
<!-- <el-button @click="draw">绘制(测试)</el-button> -->
<el-button size="large" class="primary-btn" :disabled="tempEditing" @click="save(false)">保存</el-button>
<copyRight>
<el-button :loading="loading" size="large" class="primary-btn" :disabled="tempEditing" plain type="primary" @click="download">下载作品</el-button>
<el-button :loading="state.loading" size="large" class="primary-btn" :disabled="tempEditing" plain type="primary" @click="download">下载作品</el-button>
</copyRight>
</div>
<!-- 生成图片组件 -->
<SaveImage ref="canvasImage" />
</template>
<script lang="ts">
<script lang="ts" setup>
import api from '@/api'
import { defineComponent, reactive, toRefs, getCurrentInstance, ComponentInternalInstance } from 'vue'
import { reactive, toRefs, defineEmits, defineProps, ref } from 'vue'
import { mapGetters, mapActions, useStore } from 'vuex'
import { useRoute, useRouter } from 'vue-router'
import _dl from '@/common/methods/download'
@ -38,62 +38,81 @@ import copyRight from './CopyRight.vue'
import _config from '@/config'
import useConfirm from '@/common/methods/confirm'
import wGroup from '@/components/modules/widgets/wGroup/wGroup.vue'
import { useSetupMapGetters } from '@/common/hooks/mapGetters'
export default defineComponent({
components: { copyRight, SaveImage },
props: ['modelValue'],
emits: ['change', 'update:modelValue'],
setup(props, context) {
const { proxy }: any = getCurrentInstance() as ComponentInternalInstance
const route = useRoute()
const router = useRouter()
const store = useStore()
const state = reactive({
stateBollean: false,
title: '',
loading: false,
})
type TProps = {
modelValue?: boolean
}
type TEmits = {
(event: 'change', data: {downloadPercent: number, downloadText: string}): void
(event: 'update:modelValue', data: boolean): void
}
type TState= {
stateBollean: boolean,
title: string,
loading: boolean,
}
const props = defineProps<TProps>()
const emit = defineEmits<TEmits>()
const route = useRoute()
const router = useRouter()
const store = useStore()
const canvasImage = ref<typeof SaveImage | null>(null)
const {
dPage, dWidgets, tempEditing, dHistory, dPageHistory
} = useSetupMapGetters(['dPage', 'dWidgets', 'tempEditing', 'dHistory', 'dPageHistory'])
const state = reactive<TState>({
stateBollean: false,
title: '',
loading: false,
})
//
async function save(hasCover: boolean = false) {
// Bugs: page proxy?.dPageHistory
if (dHistory.value.length <= 0) {
return
}
store.commit('setShowMoveable', false) //
// console.log(proxy?.dPage, proxy?.dWidgets)
const { id, tempid } = route.query
const cover = hasCover ? await draw() : undefined
const widgets = dWidgets.value // reviseData()
const { id: newId, stat, msg } = await api.home.saveWorks({ cover, id, title: state.title || '未命名设计', data: JSON.stringify({ page: dPage.value, widgets }), temp_id: tempid, width: dPage.value.width, height: dPage.value.height })
stat !== 0 ? useNotification('保存成功', '可在"我的作品"中查看') : useNotification('保存失败', msg, { type: 'error' })
!id && router.push({ path: '/home', query: { id: newId }, replace: true })
store.commit('setShowMoveable', true)
}
//
async function save(hasCover: boolean = false) {
// Bugs: page proxy?.dPageHistory
if (proxy?.dHistory.length <= 0) {
return
}
store.commit('setShowMoveable', false) //
// console.log(proxy?.dPage, proxy?.dWidgets)
const { id, tempid } = route.query
const cover = hasCover ? await proxy?.draw() : undefined
const widgets = proxy.dWidgets // reviseData()
const { id: newId, stat, msg } = await api.home.saveWorks({ cover, id, title: proxy.title || '未命名设计', data: JSON.stringify({ page: proxy.dPage, widgets }), temp_id: tempid, width: proxy.dPage.width, height: proxy.dPage.height })
stat !== 0 ? useNotification('保存成功', '可在"我的作品"中查看') : useNotification('保存失败', msg, { type: 'error' })
!id && router.push({ path: '/home', query: { id: newId }, replace: true })
store.commit('setShowMoveable', true)
}
//
async function saveTemp() {
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 saveTemp() {
const { tempid, tempType: type } = route.query
let res = null
if (Number(type) == 1) {
//
if (dWidgets.value[0].type === 'w-group') {
const group = dWidgets.value.shift()
group.record.width = 0
group.record.height = 0
dWidgets.value.push(group)
}
// TODO
if (!dWidgets.value.some((x: Record<string, any>) => x.type === 'w-group')) {
alert('提交组件必须为组合!')
return
// proxy.dWidgets.push(wGroup.setting)
}
res = await api.home.saveTemp({ id: tempid, type, title: state.title || '未命名组件', content: JSON.stringify(dWidgets.value), width: dPage.value.width, height: dPage.value.height })
} else res = await api.home.saveTemp({ id: tempid, title: state.title || '未命名模板', content: JSON.stringify({ page: dPage.value, widgets: dWidgets.value }), width: dPage.value.width, height: dPage.value.height })
res.stat != 0 && useNotification('保存成功', '模板内容已变更')
}
//
async function stateChange(e: any) {
async function stateChange(e: string | number | boolean) {
const { tempid, tempType: type } = route.query
const { stat } = await api.home.saveTemp({ id: tempid, type, state: e ? 1 : 0 })
stat != 0 && useNotification('保存成功', '模板内容已变更')
@ -103,28 +122,28 @@ export default defineComponent({
return
}
//
if (proxy.title === '自设计模板') {
if (state.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: '正在处理封面' })
emit('update:modelValue', true)
emit('change', { downloadPercent: 1, downloadText: '正在处理封面' })
await save(true)
setTimeout(async () => {
const { id } = route.query
if (id) {
const { width, height } = proxy.dPage
context.emit('update:modelValue', true)
context.emit('change', { downloadPercent: 1, downloadText: '准备合成图片' })
const { width, height } = dPage.value
emit('update:modelValue', true)
emit('change', { downloadPercent: 1, downloadText: '准备合成图片' })
state.loading = false
let timerCount = 0
const animation = setInterval(() => {
if (props.modelValue && timerCount < 75) {
timerCount += RandomNumber(1, 10)
context.emit('change', { downloadPercent: 1 + timerCount, downloadText: '正在合成图片' })
emit('change', { downloadPercent: 1 + timerCount, downloadText: '正在合成图片' })
} else {
clearInterval(animation)
}
@ -132,12 +151,12 @@ export default defineComponent({
await _dl.downloadImg(api.home.download({ id, width, height }) + '&r=' + Math.random(), (progress: number, xhr: any) => {
if (props.modelValue) {
clearInterval(animation)
progress >= timerCount && context.emit('change', { downloadPercent: Number(progress.toFixed(0)), downloadText: '图片生成中' })
progress >= timerCount && emit('change', { downloadPercent: Number(progress.toFixed(0)), downloadText: '图片生成中' })
} else {
xhr.abort()
}
})
context.emit('change', { downloadPercent: 100, downloadText: '图片下载中' })
emit('change', { downloadPercent: 100, downloadText: '图片下载中' })
}
}, 100)
}
@ -145,56 +164,57 @@ export default defineComponent({
return Math.ceil(Math.random() * (max - min)) + min
}
return {
...toRefs(state),
download,
save,
saveTemp,
stateChange,
// ...mapActions(['pushHistory', 'addGroup']),
async function load(id: number, tempId: number, type: number, cb: () => void) {
if (route.name !== 'Draw') {
await useFontStore.init() //
}
const apiName = tempId && !id ? 'getTempDetail' : 'getWorks'
if (!id && !tempId) {
cb()
return
}
const { data: content, title, state, width, height } = await api.home[apiName]({ id: id || tempId, type })
if (content) {
const data = JSON.parse(content)
state.stateBollean = (!!state)
state.title = title
store.commit('setShowMoveable', false) //
// this.$store.commit('setDWidgets', [])
if (type == 1) {
//
dPage.value.width = width
dPage.value.height = height
store.dispatch('addGroup', data)
// addGroup(data)
} else {
store.commit('setDPage', data.page)
id ? store.commit('setDWidgets', data.widgets) : store.dispatch('setTemplate', data.widgets)
}
},
computed: {
...mapGetters(['dPage', 'dWidgets', 'tempEditing', 'dHistory', 'dPageHistory']),
},
methods: {
...mapActions(['pushHistory', 'addGroup']),
async load(id: any, tempId: any, type: any, cb: Function) {
if (this.$route.name !== 'Draw') {
await useFontStore.init() //
}
const apiName = tempId && !id ? 'getTempDetail' : 'getWorks'
if (!id && !tempId) {
cb()
return
}
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', [])
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')
}
},
draw() {
return new Promise((resolve) => {
this.$refs.canvasImage.createCover(({ key }) => {
resolve(_config.IMG_URL + key)
})
cb()
store.dispatch('pushHistory', '请求加载load')
// pushHistory('load')
}
}
function draw() {
return new Promise((resolve) => {
if (!canvasImage.value) resolve('')
else {
canvasImage.value.createCover(({ key }: {key: string}) => {
resolve(_config.IMG_URL + key)
})
},
},
}
})
}
defineExpose({
download,
save,
saveTemp,
stateChange,
load,
})
</script>

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