The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .eslintrc.js
├── .github
    └── ISSUE_TEMPLATE
    │   ├── bug_report.md
    │   ├── 功能建议.md
    │   └── 问题反馈.md
├── .gitignore
├── .prettierrc.js
├── .vscode
    └── settings.json
├── LICENSE
├── LICENSE-ZH
├── README.md
├── babel.config.js
├── docker
    ├── api
    │   └── Dockerfile
    ├── docker-compose.yaml
    └── web
    │   ├── Dockerfile
    │   └── static.conf
├── index.html
├── package-lock.json
├── package.json
├── packages
    ├── color-picker
    │   ├── CHANGELOG.md
    │   ├── README.md
    │   ├── comps
    │   │   ├── AngleHandle.vue
    │   │   ├── TabPanel.vue
    │   │   ├── Tabs.vue
    │   │   └── svg.vue
    │   ├── index.css
    │   ├── index.ts
    │   ├── index.vue
    │   ├── package.json
    │   └── utils
    │   │   ├── color.ts
    │   │   ├── helper.ts
    │   │   ├── moveable.ts
    │   │   └── tool.ts
    └── image-extraction
    │   ├── CHANGELOG.md
    │   ├── ImageExtraction.vue
    │   ├── LICENSE
    │   ├── README.md
    │   ├── assets
    │       └── eraser.png
    │   ├── composables
    │       ├── use-init-listeners.ts
    │       ├── use-init-matting.ts
    │       ├── use-matting-cursor.ts
    │       └── use-matting.ts
    │   ├── constants
    │       └── index.ts
    │   ├── env.d.ts
    │   ├── helpers
    │       ├── dom-helper.ts
    │       ├── drawing-compute.ts
    │       ├── drawing-helper.ts
    │       ├── init-compute.ts
    │       ├── init-drawing-listeners.ts
    │       ├── init-matting.ts
    │       ├── init-transform-listener.ts
    │       ├── listener-manager.ts
    │       ├── mask-renderer.ts
    │       ├── transform-helper.ts
    │       └── util.ts
    │   ├── index.ts
    │   ├── libs
    │       ├── cuon-utils.ts
    │       ├── webgl-debug.ts
    │       └── webgl-utils.ts
    │   ├── package.json
    │   └── types
    │       ├── common.d.ts
    │       ├── cursor.d.ts
    │       ├── dom.d.ts
    │       ├── drawing-listeners.d.ts
    │       ├── drawing.d.ts
    │       ├── init-matting.d.ts
    │       ├── listener-manager.d.ts
    │       ├── matting-drawing.d.ts
    │       ├── matting.d.ts
    │       └── transform.d.ts
├── postcss.config.js
├── public
    ├── favicon.svg
    ├── robots.txt
    └── snap.svg-min.js
├── service
    ├── .gitignore
    ├── .vscode
    │   ├── api-get.code-snippets
    │   ├── api-graphql.code-snippets
    │   ├── api-post.code-snippets
    │   ├── api-success.code-snippets
    │   └── apidoc.code-snippets
    ├── README.md
    ├── package-lock.json
    ├── package.json
    ├── src
    │   ├── configs.ts
    │   ├── control
    │   │   ├── api.ts
    │   │   └── router.ts
    │   ├── main.ts
    │   ├── mock
    │   │   ├── assets
    │   │   │   ├── 1.png
    │   │   │   └── 2.png
    │   │   ├── cates.json
    │   │   ├── components
    │   │   │   ├── detail
    │   │   │   │   ├── 1.json
    │   │   │   │   ├── 2.json
    │   │   │   │   ├── 3.json
    │   │   │   │   ├── 4.json
    │   │   │   │   ├── 5.json
    │   │   │   │   └── 6.json
    │   │   │   └── list
    │   │   │   │   ├── comp.json
    │   │   │   │   └── text.json
    │   │   ├── materials
    │   │   │   ├── mask.json
    │   │   │   ├── photos
    │   │   │   │   ├── 1.json
    │   │   │   │   ├── 2.json
    │   │   │   │   └── 3.json
    │   │   │   ├── png.json
    │   │   │   └── svg.json
    │   │   └── templates
    │   │   │   ├── 1.json
    │   │   │   ├── 2.json
    │   │   │   └── list.json
    │   ├── service
    │   │   ├── design.ts
    │   │   ├── files.ts
    │   │   ├── screenshots.ts
    │   │   └── user.ts
    │   ├── shims-my.d.ts
    │   └── utils
    │   │   ├── download-single.ts
    │   │   ├── download.ts
    │   │   ├── fs.ts
    │   │   ├── http.ts
    │   │   ├── node-queue.ts
    │   │   ├── timeout.ts
    │   │   ├── tools.ts
    │   │   ├── uuid.ts
    │   │   └── widget
    │   │       ├── Device.js
    │   │       └── apidoc.js
    ├── tsconfig.json
    ├── webpack.config.js
    └── webpack.plugin.js
├── src
    ├── App.vue
    ├── api
    │   ├── ai.ts
    │   ├── album.ts
    │   ├── github.ts
    │   ├── home.ts
    │   ├── index.ts
    │   └── material.ts
    ├── assets
    │   ├── data
    │   │   ├── AlignListData.ts
    │   │   ├── LayerIconList.ts
    │   │   ├── PageSizeData.ts
    │   │   ├── QrCodeLocalization.ts
    │   │   ├── TextIconsData.ts
    │   │   └── WidgetClassifyList.ts
    │   ├── fonts
    │   │   ├── xpsj.subset.ttf
    │   │   └── xpsj.subset.woff2
    │   └── styles
    │   │   ├── base.less
    │   │   ├── color.less
    │   │   ├── design.less
    │   │   ├── index.less
    │   │   ├── layout.less
    │   │   └── main.less
    ├── common
    │   ├── hooks
    │   │   ├── dragHelper.ts
    │   │   └── history.ts
    │   └── methods
    │   │   ├── DesignFeatures
    │   │       ├── setComponents.ts
    │   │       ├── setImage.ts
    │   │       └── setWidgetData.ts
    │   │   ├── QiNiu.ts
    │   │   ├── addMouseWheel.ts
    │   │   ├── confirm.ts
    │   │   ├── download
    │   │       ├── download.ts
    │   │       ├── downloadBase64File.ts
    │   │       ├── downloadBlob.ts
    │   │       └── index.ts
    │   │   ├── fonts
    │   │       ├── index.ts
    │   │       └── utils.ts
    │   │   ├── getImgDetail.ts
    │   │   ├── handleTransform.ts
    │   │   ├── helper
    │   │       └── index.ts
    │   │   ├── loading.ts
    │   │   ├── notification.ts
    │   │   └── target.ts
    ├── components
    │   ├── business
    │   │   ├── create-design
    │   │   │   ├── createDesign.vue
    │   │   │   ├── index.ts
    │   │   │   └── sizeEditor.vue
    │   │   ├── cropper
    │   │   │   └── CropImage
    │   │   │   │   ├── CropImage.vue
    │   │   │   │   └── index.vue
    │   │   ├── image-cutout
    │   │   │   ├── ImageCutout
    │   │   │   │   ├── index.vue
    │   │   │   │   └── method.ts
    │   │   │   ├── ImageExtraction
    │   │   │   │   └── index.vue
    │   │   │   └── index.ts
    │   │   ├── moveable
    │   │   │   ├── Moveable.vue
    │   │   │   ├── Selecto.ts
    │   │   │   └── style
    │   │   │   │   ├── index.less
    │   │   │   │   └── rotation-icon.svg
    │   │   ├── picture-selector
    │   │   │   ├── index.ts
    │   │   │   └── index.vue
    │   │   ├── qrcode
    │   │   │   ├── index.ts
    │   │   │   ├── index.vue
    │   │   │   └── method.ts
    │   │   ├── right-click-menu
    │   │   │   ├── RcMenu.vue
    │   │   │   └── rcMenuData.ts
    │   │   └── save-download
    │   │   │   └── CreateCover.vue
    │   ├── common
    │   │   ├── PopoverTip.vue
    │   │   ├── ProgressLoading
    │   │   │   ├── download.vue
    │   │   │   └── index.vue
    │   │   └── Uploader
    │   │   │   ├── index.ts
    │   │   │   └── index.vue
    │   └── modules
    │   │   ├── index.ts
    │   │   ├── layout
    │   │       ├── designBoard
    │   │       │   ├── comps
    │   │       │   │   ├── pageWatermark.vue
    │   │       │   │   └── resize.vue
    │   │       │   ├── index.vue
    │   │       │   └── pageStyle.vue
    │   │       ├── lineGuides.vue
    │   │       ├── multipleBoards
    │   │       │   ├── index.ts
    │   │       │   └── multipleBoards.vue
    │   │       ├── sizeControl.vue
    │   │       └── zoomControl
    │   │       │   ├── data.ts
    │   │       │   └── index.vue
    │   │   ├── panel
    │   │       ├── components
    │   │       │   └── layerList.vue
    │   │       ├── stylePanel.vue
    │   │       ├── types
    │   │       │   └── wrap.d.ts
    │   │       ├── widgetPanel.vue
    │   │       └── wrap
    │   │       │   ├── BgImgListWrap.vue
    │   │       │   ├── CompListWrap.vue
    │   │       │   ├── GraphListWrap.vue
    │   │       │   ├── PhotoListWrap.vue
    │   │       │   ├── TempListWrap.vue
    │   │       │   ├── TextListWrap.vue
    │   │       │   ├── ToolsListWrap.vue
    │   │       │   ├── UserWrap.vue
    │   │       │   └── components
    │   │       │       ├── classHeader.vue
    │   │       │       ├── editModel.vue
    │   │       │       ├── imageTip.vue
    │   │       │       ├── imgWaterFall.vue
    │   │       │       ├── photoList.vue
    │   │       │       └── searchHeader.vue
    │   │   ├── settings
    │   │       ├── EffectSelect
    │   │       │   ├── ContainerWrap.vue
    │   │       │   └── TextWrap.vue
    │   │       ├── colorSelect.vue
    │   │       ├── iconItemSelect.vue
    │   │       ├── numberInput.vue
    │   │       ├── numberSlider.vue
    │   │       ├── textInput.vue
    │   │       ├── textInputArea.vue
    │   │       └── valueSelect.vue
    │   │   └── widgets
    │   │       ├── wGroup
    │   │           ├── groupSetting.ts
    │   │           ├── wGroup.vue
    │   │           ├── wGroupStatic.vue
    │   │           └── wGroupStyle.vue
    │   │       ├── wImage
    │   │           ├── components
    │   │           │   └── innerToolBar.vue
    │   │           ├── wImage.vue
    │   │           ├── wImageSetting.ts
    │   │           ├── wImageStatic.vue
    │   │           └── wImageStyle.vue
    │   │       ├── wQrcode
    │   │           ├── wQrcode.vue
    │   │           ├── wQrcodeSetting.ts
    │   │           └── wQrcodeStyle.vue
    │   │       ├── wSvg
    │   │           ├── wSvg.vue
    │   │           ├── wSvgSetting.ts
    │   │           ├── wSvgStatic.vue
    │   │           └── wSvgStyle.vue
    │   │       └── wText
    │   │           ├── getGradientOrImg.ts
    │   │           ├── pageFontsFilter.ts
    │   │           ├── wText.vue
    │   │           ├── wTextSetting.ts
    │   │           ├── wTextStatic.vue
    │   │           └── wTextStyle.vue
    ├── config.ts
    ├── languages
    │   ├── index.ts
    │   └── modules
    │   │   ├── en
    │   │       ├── header.ts
    │   │       └── index.ts
    │   │   └── zh
    │   │       ├── header.ts
    │   │       └── index.ts
    ├── main.ts
    ├── mixins
    │   ├── methods
    │   │   ├── dealWithCtrl.ts
    │   │   ├── handlePaste.ts
    │   │   └── keyCodeOptions.ts
    │   ├── move.ts
    │   ├── scKeyCodes.ts
    │   └── shortcuts.ts
    ├── router
    │   ├── base.ts
    │   ├── hook.ts
    │   └── index.ts
    ├── store
    │   ├── base
    │   │   ├── index.ts
    │   │   └── user.ts
    │   ├── design
    │   │   ├── canvas
    │   │   │   ├── d.ts
    │   │   │   ├── index.ts
    │   │   │   └── page-default.ts
    │   │   ├── control
    │   │   │   └── index.ts
    │   │   ├── force
    │   │   │   └── index.ts
    │   │   ├── group
    │   │   │   ├── action
    │   │   │   │   └── index.ts
    │   │   │   └── index.ts
    │   │   ├── history
    │   │   │   ├── actions
    │   │   │   │   ├── handleHistory.ts
    │   │   │   │   └── pushHistory.ts
    │   │   │   └── index.ts
    │   │   └── widget
    │   │   │   ├── actions
    │   │   │       ├── align.ts
    │   │   │       ├── clone.ts
    │   │   │       ├── group.ts
    │   │   │       ├── index.ts
    │   │   │       ├── layer.ts
    │   │   │       ├── resize.ts
    │   │   │       ├── select.ts
    │   │   │       ├── template.ts
    │   │   │       └── widget.ts
    │   │   │   ├── getter
    │   │   │       └── index.ts
    │   │   │   └── index.ts
    │   └── index.ts
    ├── types
    │   ├── env.d.ts
    │   ├── global.d.ts
    │   ├── properties.d.ts
    │   ├── shims-vue.d.ts
    │   ├── style.d.ts
    │   ├── vue-ts.d.ts
    │   ├── vuex-shim.d.ts
    │   └── worker.d.ts
    ├── utils
    │   ├── axios.ts
    │   ├── index.ts
    │   ├── plugins
    │   │   ├── cssLoader.ts
    │   │   ├── eventBus.ts
    │   │   ├── modules.ts
    │   │   ├── pointImg.ts
    │   │   ├── preload.ts
    │   │   ├── psd
    │   │   │   ├── color
    │   │   │   │   ├── color.ts
    │   │   │   │   └── index.ts
    │   │   │   ├── helper.ts
    │   │   │   └── index.ts
    │   │   ├── webWorker.ts
    │   │   └── worker
    │   │   │   └── loadPSD.worker.ts
    │   ├── utils.ts
    │   └── widgets
    │   │   ├── diffLayouts.ts
    │   │   ├── elementConfig.ts
    │   │   ├── history.worker.ts
    │   │   └── loadFontRule.ts
    └── views
    │   ├── Draw.vue
    │   ├── Html.vue
    │   ├── Index.vue
    │   ├── Psd.vue
    │   └── components
    │       ├── Folder.vue
    │       ├── HeaderOptions.vue
    │       ├── Helper.vue
    │       ├── Tour.vue
    │       ├── UploadTemplate.vue
    │       └── Watermark.vue
├── tsconfig.json
└── vite.config.ts


/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   extends: [
 3 |     'alloy',
 4 |     'alloy/vue',
 5 |     // 'alloy/typescript',
 6 |     '@vue/typescript',
 7 |   ],
 8 |   env: {
 9 |     // 你的环境变量
10 |   },
11 |   globals: {},
12 |   rules: {
13 |     // 自定义你的规则
14 |     'vue/component-tags-order': ['off'],
15 |     'vue/no-multiple-template-root': ['off'],
16 |     // 'no-undef': 'off', // 禁止使用未定义的变量,会把TS声明视为变量,暂时关闭
17 |   },
18 |   parserOptions: {
19 |     ecmaFeatures: {
20 |       legacyDecorators: true, // 配置允许注解
21 |     },
22 |   },
23 | }
24 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: Bug report
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 | 
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 | 
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 | 
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 | 
26 | **Desktop (please complete the following information):**
27 |  - OS: [e.g. iOS]
28 |  - Browser [e.g. chrome, safari]
29 |  - Version [e.g. 22]
30 | 
31 | **Smartphone (please complete the following information):**
32 |  - Device: [e.g. iPhone6]
33 |  - OS: [e.g. iOS8.1]
34 |  - Browser [e.g. stock browser, safari]
35 |  - Version [e.g. 22]
36 | 
37 | **Additional context**
38 | Add any other context about the problem here.
39 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/功能建议.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: 功能建议
 3 | about: Suggest an idea for this project
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **描述你的需求* *
11 | 对问题的清晰而简洁的描述。
12 | 
13 | **描述你想要的解决方案**
14 | 对你想要发生的事情的清晰简洁的描述。
15 | 
16 | **描述你考虑过的替代方案**
17 | 清晰简洁地描述你考虑过的任何替代方案或功能。
18 | 
19 | * * * *其他上下文
20 | 在这里添加有关功能请求的任何其他上下文或屏幕截图。
21 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/问题反馈.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: 问题反馈
 3 | about: Create a report to help us improve
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | **描述bug**
11 | 尽量清晰地描述Bug。
12 | 
13 | 重现行为的步骤:
14 | 
15 | 1. 转到“……”
16 | 2. 点击“....”
17 | 3. 向下滚动到` .... `
18 | 4. 看到错误
19 | 
20 | * * * *预期行为
21 | 对你期望发生的事情的清晰简洁的描述。
22 | 
23 | * * * *的屏幕截图
24 | 如果可以的话,添加截图来帮助解释你的问题。
25 | 
26 | **桌面(请填写以下信息):**
27 | -操作系统:[例如iOS]
28 | -浏览器[例如chrome、safari]
29 | -版本[例如22]
30 | 
31 | **智能手机(请填写以下信息):**
32 | -设备:[例如iPhone6]
33 | -操作系统:[例如iOS8.1]
34 | -浏览器[例如:普通浏览器,safari]
35 | -版本[例如22]
36 | 
37 | * * * *其他上下文
38 | 在这里添加有关该问题的任何其他上下文。
39 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | node_modules
 3 | /dist
 4 | config.json
 5 | /.vite
 6 | yarn.lock
 7 | 
 8 | screenshot/node_modules/
 9 | screenshot/dist/
10 | screenshot/_apidoc/
11 | 
12 | # local env files
13 | .env.local
14 | .env.*.local
15 | 
16 | # Log files
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 | pnpm-debug.log*
21 | 
22 | # Editor directories and files
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 | 


--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
 1 | // .prettierrc.js 代码美化规则配置
 2 | module.exports = {
 3 |     // 一行最多 n 字符
 4 |     printWidth: 1000,
 5 |     // 使用 2 个空格缩进
 6 |     tabWidth: 2,
 7 |     // 不使用缩进符,而使用空格
 8 |     useTabs: false,
 9 |     // 行尾需要有分号
10 |     semi: false,
11 |     // 使用单引号
12 |     singleQuote: true,
13 |     // 对象的 key 仅在必要时用引号
14 |     quoteProps: 'as-needed',
15 |     // jsx 不使用单引号,而使用双引号
16 |     jsxSingleQuote: false,
17 |     // 末尾需要有逗号
18 |     trailingComma: 'all',
19 |     // 大括号内的首尾需要空格
20 |     bracketSpacing: true,
21 |     // jsx 标签的反尖括号需要换行
22 |     jsxBracketSameLine: false,
23 |     // 箭头函数,只有一个参数的时候,也需要括号
24 |     arrowParens: 'always',
25 |     // 每个文件格式化的范围是文件的全部内容
26 |     rangeStart: 0,
27 |     rangeEnd: Infinity,
28 |     // 不需要写文件开头的 @prettier
29 |     requirePragma: false,
30 |     // 不需要自动在文件开头插入 @prettier
31 |     insertPragma: false,
32 |     // 使用默认的折行标准
33 |     proseWrap: 'preserve',
34 |     // 根据显示样式决定 html 要不要折行
35 |     htmlWhitespaceSensitivity: 'css',
36 |     // vue 文件中的 script 和 style 内不用缩进
37 |     vueIndentScriptAndStyle: false,
38 |     // 换行符使用 lf
39 |     endOfLine: 'lf',
40 |     // 格式化嵌入的内容
41 |     embeddedLanguageFormatting: 'auto',
42 |   }
43 |   


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "files.eol": "\n",
 3 |   "editor.tabSize": 2,
 4 |   "editor.formatOnSave": false,
 5 |   "editor.defaultFormatter": "esbenp.prettier-vscode",
 6 |   "eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
 7 |   "editor.codeActionsOnSave": {
 8 |     "source.fixAll.eslint": "explicit"
 9 |   },
10 |   "css.validate": false,
11 |   "less.validate": false,
12 |   "scss.validate": false,
13 |   "i18n-ally.localesPaths": [
14 |     "src/languages"
15 |   ]
16 | }
17 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2023-present ShawnPhang (design.palxp.cn)
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/LICENSE-ZH:
--------------------------------------------------------------------------------
 1 | 中文开源许可证:
 2 | 
 3 | “迅排设计”(以下简称迅排,官方站点: design.palxp.cn),本项目开源代码旨在促进技术社区交流与发展,因此本软件代码允许任何人士免费使用、研究、修改及发布、销售等行为,但必须保留本许可声明。
 4 | 
 5 | 可能的情况下,请在二次开发的产品上始终注明软件来源,并链接到此 Github 仓库或项目网站,这种行为值得赞扬!但并非是强制的。
 6 | 
 7 | 一些行为或用途会直接损害作者的努力成果,特此声明:
 8 | 
 9 | ❌ 您不得以原样形式倒卖或分发内容,原样形式是指未对内容进行任何创造性努力,并且其形式与我们网站上的内容完全相同。
10 | ❌ 您不得以误导或欺骗的方式使用内容,或暗示你的产品获得了来自迅排的授意。
11 | ❌ 您不得将迅排的任何内容用作商标、设计标记、商号或服务标记的一部分。
12 | 
13 | 免责声明:
14 | 
15 | 1)本许可证项下的软件代码是“按原样”提供,不作任何明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵犯版权、专利、商标或其他权利的保证。在任何情况下,版权持有人均不承担因使用或无法使用本软件而产生、引起的任何索赔、损害或其他责任,包括任何一般、特殊、间接、附带或结果性损害。
16 | 
17 | 2)这是一个非官方翻译的开源许可证,在 MIT 协议下的版本只有原始英文版才有效。然而,我们认识到,这种非官方的翻译将帮助开发者们更好地理解迅排设计的开源理念。我们鼓励自由地在迅排开源基础上进行各种创造、作品衍生,并期待社区能够出现更精彩的产品。


--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   presets: [
3 |     '@vue/cli-plugin-babel/preset'
4 |   ]
5 | }
6 | 


--------------------------------------------------------------------------------
/docker/api/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM node:20-alpine  AS builder
 2 | 
 3 | WORKDIR /usr/src/app
 4 | 
 5 | # 仅复制 package.json 和 package-lock.json 以利用缓存
 6 | COPY ./package*.json ./
 7 | 
 8 | # 安装依赖
 9 | RUN npm install --registry=https://registry.npmmirror.com
10 | 
11 | # 复制其余的源代码
12 | COPY ./ ./
13 | 
14 | RUN npm run build && cp -r ./src/mock ./dist
15 | 
16 | # 第二阶段: 运行阶段
17 | FROM ghcr.io/puppeteer/puppeteer:latest
18 | 
19 | USER root
20 | 
21 | RUN mkdir -p /cache
22 | 
23 | # 设置工作目录
24 | WORKDIR /usr/src/app
25 | 
26 | 
27 | # 从构建阶段复制构建好的 dist 目录
28 | COPY --from=builder /usr/src/app/dist ./
29 | 
30 | RUN npm install --registry=https://registry.npmmirror.com
31 | 
32 | RUN mkdir -p server && mv -f server.js server/index.js
33 | 
34 | # 如果需要暴露端口,例如 3000
35 | EXPOSE 3000
36 | 
37 | # 定义容器启动时执行的命令
38 | CMD ["node", "server/index.js"]


--------------------------------------------------------------------------------
/docker/docker-compose.yaml:
--------------------------------------------------------------------------------
 1 | version: '3.8'
 2 | 
 3 | services:
 4 |   poster-web:
 5 |     image: heimanba/poster-web
 6 |     ports:
 7 |       - "80:80"
 8 |     depends_on:
 9 |       - poster-api
10 |     network_mode: host
11 | 
12 |   poster-api:
13 |     image: heimanba/poster-api
14 |     ports:
15 |       - "7001:7001"
16 |     network_mode: host


--------------------------------------------------------------------------------
/docker/web/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM node:20-alpine  AS builder
 2 | 
 3 | WORKDIR /usr/src/app
 4 | 
 5 | 
 6 | # 复制其余的源代码
 7 | COPY ./ ./
 8 | 
 9 | 
10 | RUN npm i --registry=https://registry.npmmirror.com
11 | 
12 | 
13 | RUN npm run build
14 | 
15 | # 使用官方的nginx基础镜像
16 | FROM alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/nginx_optimized
17 | 
18 | # 设置工作目录
19 | WORKDIR /usr/share/nginx/html
20 | 
21 | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
22 | 
23 | COPY ./docker/web/static.conf /etc/nginx/default.d/
24 | 
25 | # 暴露80端口和443端口(如果你使用HTTPS)
26 | EXPOSE 80 443
27 | 
28 | # 使用自定义entrypoint启动
29 | CMD ["nginx", "-g", "daemon off;"]
30 | 


--------------------------------------------------------------------------------
/docker/web/static.conf:
--------------------------------------------------------------------------------
 1 | location / {
 2 | 	try_files $uri $uri/ /index.html;
 3 | 	add_header Cache-Control no-cache;
 4 | 	proxy_set_header Host $host;
 5 | 	proxy_set_header X-Real-IP $remote_addr;
 6 | 	proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
 7 | }
 8 | 
 9 | location ^~/design/ {
10 | 	proxy_pass http://127.0.0.1:7001;
11 | 	proxy_set_header Host $host;
12 | 	proxy_set_header X-Real-IP $remote_addr;
13 | 	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
14 | }


--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-01-17 16:06:30
 4 |  * @Description: Design Palxp
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-13 18:16:44
 7 | -->
 8 | <!DOCTYPE html>
 9 | <html lang="zh-CN">
10 |   <head>
11 |     <meta charset="UTF-8" />
12 |     <link rel="icon" href="/favicon.svg" />
13 |     <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
14 |     <meta name="viewport" content="width=device-width, initial-scale=1.0, initial-scale=1, maximum-scale=1, user-scalable=no">
15 |     <title>迅排设计 - 轻松创意,迅捷排版,感受云上设计带来的乐趣!</title>
16 |     <script> var _hmt = _hmt || [] </script>
17 |   </head>
18 | 
19 |   <body>
20 |     <div class="pointer-case" id="app"></div>
21 |     <script type="module" src="/src/main.ts"></script>
22 |     <script defer src="/snap.svg-min.js"></script>
23 |     <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
24 |   </body>
25 | </html>
26 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "xunpai-design",
 3 |   "version": "1.0.0",
 4 |   "private": true,
 5 |   "author": "ShawnPhang",
 6 |   "scripts": {
 7 |     "prepared": "npm i && cd service && PUPPETEER_DOWNLOAD_BASE_URL=https://cdn.npmmirror.com/binaries/chrome-for-testing npm i",
 8 |     "serve": "npm run dev & cd service && npm run dev",
 9 |     "dev": "cross-env NODE_ENV=development vite",
10 |     "build": "cross-env NODE_ENV=production && vite build",
11 |     "v-build": "cross-env NODE_ENV=production && vite build",
12 |     "v-build-hard": "cross-env NODE_ENV=production vue-tsc --noEmit && vite build"
13 |   },
14 |   "dependencies": {
15 |     "@palxp/color-picker": "workspace:*",
16 |     "@palxp/image-extraction": "workspace:*",
17 |     "@scena/guides": "^0.18.1",
18 |     "axios": "^0.21.1",
19 |     "core-js": "^3.6.5",
20 |     "cropperjs": "^1.6.1",
21 |     "dayjs": "^1.10.7",
22 |     "element-plus": "^2.6.3",
23 |     "fontfaceobserver": "^2.1.0",
24 |     "html2canvas": "^1.0.0",
25 |     "immer": "^10.0.4",
26 |     "microdiff": "^1.4.0",
27 |     "mitt": "^3.0.1",
28 |     "moveable": "^0.26.0",
29 |     "moveable-helper": "^0.4.0",
30 |     "nanoid": "^3.1.23",
31 |     "normalize.css": "^8.0.1",
32 |     "pinia": "^2.1.7",
33 |     "psd.js": "^3.9.0",
34 |     "qr-code-styling": "^1.6.0-rc.1",
35 |     "selecto": "^1.13.0",
36 |     "throttle-debounce": "^3.0.1",
37 |     "vite-plugin-compression": "^0.5.1",
38 |     "vue": "3.4.19",
39 |     "vue-i18n": "^9.13.1",
40 |     "vue-router": "^4.0.0-0",
41 |     "vuedraggable": "^4.1.0"
42 |   },
43 |   "devDependencies": {
44 |     "@types/cropperjs": "^1.3.0",
45 |     "@types/fontfaceobserver": "^2.1.3",
46 |     "@types/node": "^20.11.24",
47 |     "@types/throttle-debounce": "^2.1.0",
48 |     "@typescript-eslint/eslint-plugin": "^7.1.0",
49 |     "@typescript-eslint/parser": "^7.1.1",
50 |     "@vitejs/plugin-vue": "^5.0.4",
51 |     "autoprefixer": "^10.3.1",
52 |     "cross-env": "^7.0.3",
53 |     "eslint": "^8.56.0",
54 |     "eslint-config-alloy": "~4.1.0",
55 |     "eslint-plugin-vue": "^7.12.1",
56 |     "less": "^4.1.1",
57 |     "terser": "^5.28.1",
58 |     "typescript": "^5.2.2",
59 |     "unplugin-element-plus": "^0.7.1",
60 |     "vite": "^5.1.4",
61 |     "vue-tsc": "^1.8.27"
62 |   },
63 |   "workspaces": [
64 |     "packages/*"
65 |   ],
66 |   "browserslist": [
67 |     "Chrome >= 90"
68 |   ],
69 |   "website": "https://design.palxp.cn",
70 |   "homepage": "https://xp.palxp.cn"
71 | }
72 | 


--------------------------------------------------------------------------------
/packages/color-picker/README.md:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-05-29 22:54:18
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-01-31 10:51:35
 7 | -->
 8 | <img style="display: inline-block;" src="https://img.shields.io/github/watchers/palxiao/front-end-arsenal?style=social" />
 9 | <img style="display: inline-block;" src="https://img.shields.io/github/forks/palxiao/front-end-arsenal?style=social" />
10 | <img style="display: inline-block;" src="https://img.shields.io/github/stars/palxiao/front-end-arsenal?style=social" />
11 | 
12 | # color-picker
13 | 
14 | > TODO: 颜色取色器,适用于 Vue3
15 | 
16 | <img style="display: inline-block;" src="https://img.shields.io/npm/v/@palxp/color-picker" />
17 | <img style="display: inline-block;" src="https://img.shields.io/bundlephobia/min/@palxp/color-picker?color=%2344cc88" />
18 | <img style="display: inline-block;" src="https://img.shields.io/npm/dm/@palxp/color-picker" />
19 | 
20 | ## Usage
21 | 
22 | ```
23 | yarn add @palxp/color-picker
24 | 
25 | import colorPicker from '@palxp/color-picker'
26 | ```
27 | 
28 | ## API
29 | 
30 | [API Docs 链接](/#/docs)
31 | 
32 |   <iframe src="/#/docs/color-picker/index?preview=true" frameborder="0"></iframe>
33 | 


--------------------------------------------------------------------------------
/packages/color-picker/comps/TabPanel.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="tab-panel" :style="rootStyle">
 3 |     <slot />
 4 |   </div>
 5 | </template>
 6 | 
 7 | <script>
 8 | export default {
 9 |   name: 'TabPanel',
10 | }
11 | </script>
12 | 
13 | <script setup>
14 | import { computed, getCurrentInstance, ref } from 'vue'
15 | 
16 | const vm = getCurrentInstance()
17 | vm.parent.exposed.tabs.value.push(vm)
18 | 
19 | defineProps({
20 |   // Tabs 会用到 label
21 |   label: {
22 |     type: String,
23 |     required: true,
24 |   },
25 | })
26 | 
27 | const active = ref(false)
28 | const rootStyle = computed(() => ({
29 |   display: active.value ? 'block' : 'none',
30 | }))
31 | 
32 | function changeActive(value) {
33 |   active.value = value
34 | }
35 | 
36 | defineExpose({
37 |   changeActive,
38 | })
39 | </script>
40 | 


--------------------------------------------------------------------------------
/packages/color-picker/comps/svg.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-05-29 15:41:22
 4 |  * @Description: 吸管 SVG
 5 |  * @LastEditors: ShawnPhang <site: m.palxp.cn>
 6 |  * @LastEditTime: 2023-05-29 15:48:17
 7 | -->
 8 | <template>
 9 |   <svg t="1685345224620" class="sd-xggj" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8489" width="20" height="20">
10 |     <path d="M716.288 140.501333a42.666667 42.666667 0 0 1 60.373333 0l90.496 90.496a42.666667 42.666667 0 0 1 0 60.330667l-120.661333 120.704L595.626667 261.12l120.661333-120.661333zM520.192 185.770667l301.696 301.653333-60.330667 60.373333-301.653333-301.696 60.288-60.330666z" p-id="8490"></path>
11 |     <path d="M580.565333 366.762667l-60.373333-60.330667-362.026667 362.026667V853.333333l181.034667-3.84 362.026667-362.026666-60.330667-60.373334-331.861333 331.904-60.373334-60.373333 331.904-331.861333z" p-id="8491"></path>
12 |   </svg>
13 | </template>
14 | 
15 | <script lang="ts">
16 | import { defineComponent } from 'vue'
17 | 
18 | export default defineComponent({
19 |   setup() {},
20 | })
21 | </script>
22 | 


--------------------------------------------------------------------------------
/packages/color-picker/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-05-29 14:24:41
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <site: m.palxp.cn>
 6 |  * @LastEditTime: 2023-05-29 14:25:05
 7 |  */
 8 | import { App } from 'vue'
 9 | import Comp from './index.vue'
10 | 
11 | Comp.install = (app: App): void => {
12 |   app.component(Comp.name, Comp)
13 | }
14 | 
15 | export default Comp
16 | 


--------------------------------------------------------------------------------
/packages/color-picker/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@palxp/color-picker",
 3 |   "version": "1.5.5",
 4 |   "description": "TODO",
 5 |   "author": "ShawnPhang <palxiao@vip.qq.com>",
 6 |   "homepage": "https://fe-doc.palxp.cn/#/color-picker",
 7 |   "license": "ISC",
 8 |   "main": "index.ts",
 9 |   "module": "index.ts",
10 |   "types": "index.d.ts",
11 |   "publishConfig": {
12 |     "access": "public"
13 |   },
14 |   "repository": {
15 |     "type": "git",
16 |     "url": "git+https://github.com/palxiao/front-end-arsenal.git"
17 |   },
18 |   "scripts": {
19 |     "test": "echo \"Error: run tests from root\" && exit 1"
20 |   },
21 |   "bugs": {
22 |     "url": "https://github.com/palxiao/front-end-arsenal/issues"
23 |   },
24 |   "dependencies": {
25 |     "throttle-debounce": "^5.0.0"
26 |   },
27 |   "gitHead": "fcba4b7113a557f245ea8e8e64170d93bbbe1e57"
28 | }
29 | 


--------------------------------------------------------------------------------
/packages/color-picker/utils/helper.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-04-26 11:30:10
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-11-28 11:03:14
 7 |  */
 8 | export const parseBackgroundValue = (value: string): string => {
 9 |   if (value.startsWith('#')) return '纯色'
10 |   if (value.startsWith('linear-gradient')) return '渐变'
11 |   return '图案'
12 | }
13 | 
14 | interface Stop {
15 |   color: string
16 |   offset: number
17 | }
18 | 
19 | export const toGradientString = (angle: number, stops: Stop[]) => {
20 |   const s: string[] = []
21 |   stops.forEach((stop) => {
22 |     s.push(`${stop.color} ${stop.offset * 100}%`)
23 |   })
24 |   return `linear-gradient(${angle}deg, ${s.join(',')})`
25 | }
26 | 
27 | /**
28 |  * 显示全局提示
29 |  * @param content
30 |  * @param tooltipVisible
31 |  * @returns
32 |  */
33 | export function toolTip(content: string) {
34 |   const tooltip = drawTooltip(content)
35 |   document.body.appendChild(tooltip)
36 |   setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000)
37 | }
38 | 
39 | function drawTooltip(content: string, tooltipVisible = true) {
40 |   const tooltip: any = document.createElement('div')
41 |   tooltip.id = 'color-pipette-tooltip-container'
42 |   tooltip.innerHTML = content
43 |   tooltip.style = `
44 |     position: fixed;
45 |     left: 50%;
46 |     top: 9%;
47 |     z-index: 10002;
48 |     display: ${tooltipVisible ? 'flex' : 'none'};
49 |     align-items: center;
50 |     background-color: rgba(0,0,0,0.4);
51 |     padding: 6px 12px;
52 |     border-radius: 4px;
53 |     color: #fff;
54 |     font-size: 18px;
55 |     pointer-events: none;
56 |   `
57 |   return tooltip
58 | }
59 | 


--------------------------------------------------------------------------------
/packages/color-picker/utils/moveable.ts:
--------------------------------------------------------------------------------
 1 | import { toNumber } from './tool'
 2 | 
 3 | interface Position {
 4 |   x: number
 5 |   y: number
 6 | }
 7 | 
 8 | interface RegisterMoveablePanelOptions {
 9 |   wrapEl?: HTMLElement
10 |   onmousedown?(position: Position, event: MouseEvent): void
11 |   onmousemove?(position: Position, event: MouseEvent): void
12 |   onmouseup?(position: Position, event: MouseEvent): void
13 | }
14 | 
15 | export const registerMoveableElement = (el: HTMLElement, { onmousedown, onmousemove, onmouseup }: RegisterMoveablePanelOptions = {}) => {
16 |   let elRect = el.getBoundingClientRect()
17 |   const position = { x: 0, y: 0 }
18 | 
19 |   const update = (event: MouseEvent) => {
20 |     let dx = event.pageX - elRect.x
21 |     let dy = event.pageY - elRect.y
22 | 
23 |     if (dx < 0) dx = 0
24 |     if (dx > elRect.width) dx = elRect.width
25 |     if (dy < 0) dy = 0
26 |     if (dy > elRect.height) dy = elRect.height
27 | 
28 |     position.x = toNumber(dx / elRect.width, { decimal: 2 })
29 |     position.y = toNumber(dy / elRect.height, { decimal: 2 })
30 |   }
31 | 
32 |   const _onmousemove = (event: MouseEvent) => {
33 |     update(event)
34 | 
35 |     if (onmousemove) {
36 |       onmousemove(position, event)
37 |     }
38 |   }
39 | 
40 |   const _onmouseup = (event: MouseEvent) => {
41 |     document.removeEventListener('mousemove', _onmousemove)
42 |     document.removeEventListener('mouseup', _onmouseup)
43 | 
44 |     if (onmouseup) {
45 |       onmouseup(position, event)
46 |     }
47 |   }
48 | 
49 |   const _onmousedown = (event: MouseEvent) => {
50 |     // elRect 可能不准确,这里更新一下
51 |     elRect = el.getBoundingClientRect()
52 | 
53 |     update(event)
54 | 
55 |     document.addEventListener('mousemove', _onmousemove)
56 |     document.addEventListener('mouseup', _onmouseup)
57 | 
58 |     if (onmousedown) {
59 |       onmousedown(position, event)
60 |     }
61 |   }
62 | 
63 |   el.addEventListener('mousedown', _onmousedown)
64 | 
65 |   return {
66 |     destroy() {
67 |       el.removeEventListener('mousedown', _onmousedown)
68 |     },
69 |   }
70 | }
71 | 


--------------------------------------------------------------------------------
/packages/color-picker/utils/tool.ts:
--------------------------------------------------------------------------------
1 | export const toNumber = (n: number, { decimal = 0 } = {}) => {
2 |   if (decimal > 0) {
3 |     return Number(n.toFixed(decimal))
4 |   }
5 |   return Math.round(n)
6 | }
7 | 


--------------------------------------------------------------------------------
/packages/image-extraction/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | # Change Log
 2 | 
 3 | All notable changes to this project will be documented in this file.
 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
 5 | 
 6 | ## [1.2.4](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.3...@palxp/image-extraction@1.2.4) (2023-10-08)
 7 | 
 8 | 
 9 | ### Bug Fixes
10 | 
11 | * **component:** Matting类型导出 ([02cdcf7](https://github.com/palxiao/front-end-arsenal/commit/02cdcf74dfddcd0e1cd0353f287eacb49d0c3db4))
12 | 
13 | 
14 | 
15 | 
16 | 
17 | ## [1.2.3](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.2...@palxp/image-extraction@1.2.3) (2023-10-08)
18 | 
19 | 
20 | ### Bug Fixes
21 | 
22 | * **component:** Matting类型导出 ([21aa8ad](https://github.com/palxiao/front-end-arsenal/commit/21aa8ad75021056913c0e2cc548c15a073d79e5b))
23 | 
24 | 
25 | 
26 | 
27 | 
28 | ## [1.2.2](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.1...@palxp/image-extraction@1.2.2) (2023-10-08)
29 | 
30 | **Note:** Version bump only for package @palxp/image-extraction
31 | 
32 | 
33 | 
34 | 
35 | 
36 | ## [1.2.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.2.0...@palxp/image-extraction@1.2.1) (2023-10-08)
37 | 
38 | **Note:** Version bump only for package @palxp/image-extraction
39 | 
40 | 
41 | 
42 | 
43 | 
44 | # [1.2.0](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.1.1...@palxp/image-extraction@1.2.0) (2023-10-08)
45 | 
46 | 
47 | ### Features
48 | 
49 | * **component:** 增加隐藏头部 ([406f1c5](https://github.com/palxiao/front-end-arsenal/commit/406f1c5ea0e38489e91a6b36982b773e5aad42d6))
50 | 
51 | 
52 | 
53 | 
54 | 
55 | ## [1.1.1](https://github.com/palxiao/front-end-arsenal/compare/@palxp/image-extraction@1.1.0...@palxp/image-extraction@1.1.1) (2023-10-08)
56 | 
57 | **Note:** Version bump only for package @palxp/image-extraction
58 | 
59 | 
60 | 
61 | 
62 | 
63 | # 1.1.0 (2023-10-08)
64 | 
65 | 
66 | ### Features
67 | 
68 | * **custom:** 增加Matting组件 ([b26b4dd](https://github.com/palxiao/front-end-arsenal/commit/b26b4dddd11a273adeb97104a6aa2707fb8be920))
69 | 


--------------------------------------------------------------------------------
/packages/image-extraction/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2022 科学家丶
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/packages/image-extraction/README.md:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-07 23:50:21
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-08 16:02:58
 7 | -->
 8 | 
 9 | <img style="display: inline-block;" src="https://img.shields.io/github/watchers/palxiao/front-end-arsenal?style=social" /> <img style="display: inline-block;" src="https://img.shields.io/github/forks/palxiao/front-end-arsenal?style=social" /> <img style="display: inline-block;" src="https://img.shields.io/github/stars/palxiao/front-end-arsenal?style=social" />
10 | 
11 | # image-extraction
12 | 
13 | > TODO:
14 | 
15 | <img style="display: inline-block;" src="https://img.shields.io/npm/v/@palxp/image-extraction" /> <img style="display: inline-block;" src="https://img.shields.io/bundlephobia/min/@palxp/image-extraction?color=%2344cc88" /> <img style="display: inline-block;" src="https://img.shields.io/npm/dm/@palxp/image-extraction" />
16 | 
17 | ## Usage
18 | 
19 | ```
20 | yarn add @palxp/image-extraction
21 | 
22 | import image-extraction from '@palxp/image-extraction'
23 | ```
24 | 
25 | ## API
26 | 
27 | [API Docs 链接](/#/docs)
28 | 
29 |   <iframe src="/#/docs/image-extraction/-image-extraction?preview=true" frameborder="0"></iframe>
30 | 


--------------------------------------------------------------------------------
/packages/image-extraction/assets/eraser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palxiao/poster-design/68d3e7bffa36950f3740ed92744766102a910bae/packages/image-extraction/assets/eraser.png


--------------------------------------------------------------------------------
/packages/image-extraction/env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="vite/client" />
2 | 
3 | declare module '*.vue' {
4 |   import { DefineComponent } from 'vue'
5 |   // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6 |   const component: DefineComponent<{}, {}, any>
7 |   export default component
8 | }
9 | 


--------------------------------------------------------------------------------
/packages/image-extraction/helpers/init-transform-listener.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-05 16:33:07
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-08 11:10:07
 7 |  */
 8 | import { InitMattingDragConfig, InitMattingScaleConfig } from '../types/transform'
 9 | import { redrawMattingBoardsWhileScaling, updateRangeByMovements } from './transform-helper'
10 | 
11 | /** 初始化画板变换的监听器 */
12 | export function initDragListener(mattingTransformConfig: InitMattingDragConfig) {
13 |   const {
14 |     outputContexts: { ctx: outputCtx2D },
15 |     // inputContexts: { ctx: inputCtx2D },
16 |     transformConfig,
17 |     listenerManager,
18 |   } = mattingTransformConfig
19 |   listenerManager.initMouseListeners({
20 |     mouseTarget: outputCtx2D.canvas,
21 |     move(ev) {
22 |       const { positionRange } = transformConfig
23 |       updateRangeByMovements(ev, positionRange)
24 |     },
25 |   })
26 | }
27 | 
28 | /** 初始化缩放监听器 */
29 | export function initScaleListener(mattingTransformConfig: InitMattingScaleConfig): VoidFunction {
30 |   const {
31 |     inputContexts: { ctx: inputCtx },
32 |     outputContexts: { ctx: outputCtx },
33 |     listenerManager,
34 |   } = mattingTransformConfig
35 |   return listenerManager.initWheelListener({
36 |     mattingBoards: [inputCtx.canvas, outputCtx.canvas],
37 |     wheel(ev) {
38 |       ev.preventDefault()
39 |       redrawMattingBoardsWhileScaling(ev, mattingTransformConfig)
40 |     },
41 |   })
42 | }
43 | 


--------------------------------------------------------------------------------
/packages/image-extraction/helpers/util.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-05 16:33:07
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-08 11:10:19
 7 |  */
 8 | import { DRAWING_STEP_BASE, DRAWING_STEP_BASE_BASE, MIN_RADIUS } from '../constants'
 9 | import { RectSize, TransformConfig } from '../types/common'
10 | 
11 | const { sqrt, max } = Math
12 | 
13 | export function fixed(num: number): number {
14 |   return num | 0
15 | }
16 | // 比Math.hypot(x,y)快一些(在数量级较大的情况下)
17 | export function getRawDistance(xDistance: number, yDistance: number): number {
18 |   return sqrt(xDistance ** 2 + yDistance ** 2)
19 | }
20 | 
21 | /** 计算插值绘制的间隔步长 */
22 | export function computeStepBase(radius: number) {
23 |   return radius / DRAWING_STEP_BASE_BASE
24 | }
25 | 
26 | /** 计算真实(相对真实,如果图像分辨率会控制在2K以内以保证性能)尺寸的画笔绘制点的半径 */
27 | export function computeRealRadius(rawRadius: number, scaleRatio: number) {
28 |   return max(MIN_RADIUS, rawRadius) / scaleRatio
29 | }
30 | 
31 | /** 计算移动绘制的节流步长 */
32 | export function computeStep(radius: number) {
33 |   return radius / DRAWING_STEP_BASE
34 | }
35 | 
36 | /** 基于新的缩放比例计算新的绘制范围 */
37 | export function computeNewTransformConfigByScaleRatio(transformConfig: TransformConfig, pictureSize: RectSize, scaleRatio: number): TransformConfig {
38 |   const { minX, minY } = transformConfig.positionRange
39 |   const { width, height } = pictureSize
40 |   const maxX = minX + width * scaleRatio
41 |   const maxY = minY + height * scaleRatio
42 |   return { positionRange: { minX, maxX, minY, maxY }, scaleRatio }
43 | }
44 | 
45 | /** 获取图片缩放到画框区域内的实际尺寸 */
46 | export function computeScaledImageSize(imageSize: RectSize, scaleRatio: number): RectSize {
47 |   return {
48 |     width: imageSize.width * scaleRatio,
49 |     height: imageSize.height * scaleRatio,
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------
/packages/image-extraction/index.ts:
--------------------------------------------------------------------------------
 1 | import { App } from 'vue'
 2 | import matting from './ImageExtraction.vue'
 3 | 
 4 | matting.install = (app: App): void => {
 5 |   app.component(matting.name, matting)
 6 | }
 7 | 
 8 | export default matting
 9 | 
10 | export interface MattingType {
11 |   value: {}
12 |   /** 是否为擦除画笔 */
13 |   isErasing: boolean
14 |   /** 下载结果图 */
15 |   onDownloadResult: Function
16 |   /** 返回结果图 */
17 |   getResult: Function
18 |   /** input表单选择文件的回调 */
19 |   onFileChange: Function
20 |   /**
21 |    * 初始化加载的图片,第一个参数为原始图像,第二个参数为裁剪图像
22 |    */
23 |   initLoadImages: Function
24 |   /** 画笔尺寸 */
25 |   radius: number | string
26 |   /** 画笔尺寸:计算属性,显示值 */
27 |   brushSize: any
28 |   /** 画笔硬度 */
29 |   hardness: number | string
30 |   /** 画笔硬度:计算属性,显示值 */
31 |   hardnessText: any
32 |   /** 常量 */
33 |   constants: {
34 |     RADIUS_SLIDER_MIN: number
35 |     RADIUS_SLIDER_MAX: number
36 |     RADIUS_SLIDER_STEP: number
37 |     HARDNESS_SLIDER_MAX: number
38 |     HARDNESS_SLIDER_STEP: number
39 |     HARDNESS_SLIDER_MIN: number
40 |   }
41 | }
42 | 


--------------------------------------------------------------------------------
/packages/image-extraction/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@palxp/image-extraction",
 3 |   "version": "1.2.4",
 4 |   "description": "TODO",
 5 |   "author": "ShawnPhang <palxiao@vip.qq.com>",
 6 |   "homepage": "https://fe-doc.palxp.cn/#/image-extraction",
 7 |   "license": "ISC",
 8 |   "main": "index.ts",
 9 |   "module": "index.ts",
10 |   "types": "types/matting.d.ts",
11 |   "publishConfig": {
12 |     "access": "public"
13 |   },
14 |   "repository": {
15 |     "type": "git",
16 |     "url": "git+https://github.com/palxiao/front-end-arsenal.git"
17 |   },
18 |   "scripts": {
19 |     "test": "echo \"Error: run tests from root\" && exit 1"
20 |   },
21 |   "bugs": {
22 |     "url": "https://github.com/palxiao/front-end-arsenal/issues"
23 |   },
24 |   "dependencies": {
25 |     "throttle-debounce": "^5.0.0"
26 |   }
27 | }
28 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/common.d.ts:
--------------------------------------------------------------------------------
 1 | import { Ref } from 'vue';
 2 | 
 3 | /** 鼠标指针移动距离 */
 4 | export interface MouseMovements {
 5 | 	/** 水平移动距离 */
 6 | 	movementX: number;
 7 | 	/** 垂直移动距离 */
 8 | 	movementY: number;
 9 | }
10 | 
11 | /** 像素坐标 */
12 | export interface PixelPosition {
13 | 	x: number;
14 | 	y: number;
15 | }
16 | 
17 | /** 变换配置对象 */
18 | export interface TransformConfig {
19 | 	positionRange: PositionRange;
20 | 	scaleRatio: number;
21 | }
22 | 
23 | /** 绘制位置范围 */
24 | export interface PositionRange {
25 | 	minX: number;
26 | 	maxX: number;
27 | 	minY: number;
28 | 	maxY: number;
29 | }
30 | 
31 | /** 矩形的尺寸 */
32 | export interface RectSize {
33 | 	width: number;
34 | 	height: number;
35 | }
36 | 
37 | /** 画板矩形的参数 */
38 | export interface BoardRect extends RectSize {
39 | 	left: number;
40 | 	top: number;
41 | }
42 | 
43 | /** 矩形的尺寸 */
44 | export interface RectSize {
45 | 	width: number;
46 | 	height: number;
47 | }
48 | 
49 | /** 画板绘制上下文 */
50 | export interface BoardDrawingContexts {
51 | 	/** 画板绘制上下文 */
52 | 	ctx: Ref<CanvasRenderingContext2D | null>;
53 | 	/** 绘制输入图像的隐藏画板的绘制上下文 */
54 | 	hiddenCtx: Ref<CanvasRenderingContext2D>;
55 | 	/** 绘制画笔形状的临时画板 */
56 | 	drawingCtx: CanvasRenderingContext2D;
57 | }
58 | 
59 | /** 画板绘制上下文对象 */
60 | export interface BoardContext2Ds {
61 | 	/** 输入画板绘制上下文 */
62 | 	inputCtx: Ref<CanvasRenderingContext2D | null>;
63 | 	/** 输出画板绘制上下文 */
64 | 	outputCtx: Ref<CanvasRenderingContext2D | null>;
65 | 	inputDrawingCtx: CanvasRenderingContext2D;
66 | 	outputDrawingCtx: CanvasRenderingContext2D;
67 | 	/** 绘制输入图像的隐藏画板的绘制上下文 */
68 | 	inputHiddenCtx: Ref<CanvasRenderingContext2D>;
69 | 	/** 绘制输出图像的隐藏画板的绘制上下文 */
70 | 	outputHiddenCtx: Ref<CanvasRenderingContext2D>;
71 | }
72 | 
73 | /** 绘制基础配置对象 */
74 | export interface MattingBoardBaseConfig {
75 | 	boardContexts: BoardContext2Ds;
76 | 	/** 画布目标尺寸 */
77 | 	targetSize: RectSize;
78 | 	/** 图像绘制时与画布边缘最小间隙 */
79 | 	gapSize?: GapSize;
80 | }
81 | 
82 | /**
83 |  * 导航视窗区域内图片默认尺寸:以图片中心点为原点,进行等比例缩放
84 |  * 图片上下边距至少各留80px,左右边距至少留白40px,上下边距优先级高于左右边距
85 |  * 例如:当图片上下留白80px时,左右留白大于40px时,以上下留白80px为准
86 |  */
87 | export interface GapSize {
88 | 	horizontal: number;
89 | 	vertical: number;
90 | }
91 | 
92 | /** 初始化按照变换配置绘制图像的基础配置对象 */
93 | export interface InitTransformedDrawBaseConfig {
94 | 	/** 变换配置 */
95 | 	transformConfig: TransformConfig;
96 | 	/** 是否绘制图像边框 */
97 | 	withBorder?: boolean;
98 | }
99 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/cursor.d.ts:
--------------------------------------------------------------------------------
 1 | import { Ref } from 'vue';
 2 | 
 3 | /** 鼠标指针的样式 */
 4 | export interface CursorStyle {
 5 | 	display?: string;
 6 | 	left?: string;
 7 | 	top?: string;
 8 | 	cursor?: string;
 9 | 	width?: string;
10 | }
11 | 
12 | /** 使用鼠标指针组合API的配置对象 */
13 | export interface UseCursorConfig {
14 | 	inputCtx: Ref<CanvasRenderingContext2D | null>;
15 | 	isDragging: Ref<boolean>;
16 | 	isErasing: Ref<boolean>;
17 | 	radius: Ref<number>;
18 | 	hardness: Ref<number>;
19 | }
20 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/dom.d.ts:
--------------------------------------------------------------------------------
 1 | import { InitTransformedDrawBaseConfig, PositionRange, RectSize, TransformConfig } from './common';
 2 | 
 3 | /** canvas尺寸重置配置对象 */
 4 | export interface ResizeCanvasConfig extends InitTransformedDrawBaseConfig {
 5 | 	/** canvas 2D上下文 */
 6 | 	ctx: CanvasRenderingContext2D;
 7 | 	hiddenCtx: CanvasRenderingContext2D;
 8 | 	/** 尺寸重置的目标宽度 */
 9 | 	targetWidth: number;
10 | 	/** 尺寸重置的目标高度 */
11 | 	targetHeight: number;
12 | }
13 | 
14 | /** 创建2D绘制上下文的配置对象 */
15 | export interface CreateContext2DConfig {
16 | 	targetSize?: RectSize;
17 | 	cloneCanvas?: HTMLCanvasElement;
18 | }
19 | 
20 | /** 初始化隐藏画板的配置对象 */
21 | export interface InitHiddenBoardConfig {
22 | 	targetSize: RectSize;
23 | 	hiddenCtx: CanvasRenderingContext2D;
24 | 	drawingCtx: CanvasRenderingContext2D;
25 | }
26 | 
27 | /** 初始化隐藏画布并返回图像的配置对象 */
28 | export interface InitHiddenBoardWithImageConfig extends InitHiddenBoardConfig {
29 | 	imageSource: ImageBitmap;
30 | }
31 | 
32 | /** 从画布获取图像资源的配置对象 */
33 | export interface GetImageSourceConfig {
34 | 	ctx: CanvasRenderingContext2D;
35 | 	imageSource: ImageBitmap;
36 | 	width: number;
37 | 	height: number;
38 | }
39 | 
40 | /** 画板变换时绘制的上下文对象 */
41 | export interface DirectlyDrawingContext {
42 | 	/** 画板绘制上下文 */
43 | 	ctx: CanvasRenderingContext2D;
44 | 	/** 隐藏的绘制上下文 */
45 | 	hiddenCtx: CanvasRenderingContext2D;
46 | }
47 | 
48 | /** 画板变换时绘制图像的配置对象 */
49 | export interface TransformedDrawingImageConfig extends DirectlyDrawingContext, TransformConfig {
50 | 	clearOld?: boolean;
51 | 	withBorder?: boolean;
52 | }
53 | 
54 | /** 绘制图像边框的配置对象 */
55 | export interface DrawImageLineBorderConfig {
56 | 	/** 边框的上下左右位置信息 */
57 | 	positionRange: PositionRange;
58 | 	ctx: CanvasRenderingContext2D;
59 | 	/** 边框颜色 */
60 | 	lineStyle: string;
61 | 	lineWidth: number;
62 | }
63 | 
64 | /** 绘制圆点的配置对象 */
65 | export interface DrawingCircularConfig {
66 | 	ctx: CanvasRenderingContext2D | CanvasRenderingContext2D;
67 | 	x: number;
68 | 	y: number;
69 | 	radius: number;
70 | 	hardness: number;
71 | 	innerColor?: string;
72 | 	outerColor?: string;
73 | }
74 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/drawing.d.ts:
--------------------------------------------------------------------------------
 1 | import { BoardDrawingContexts, MouseMovements, PixelPosition, PositionRange } from './common';
 2 | import { BrushDrawingBaseConfig } from './drawing-listeners';
 3 | 
 4 | /** 抠图绘制的配置对象 */
 5 | export interface MattingDrawingConfig extends MouseMovements, BrushDrawingBaseConfig, BoardDrawingContexts, PixelPosition {
 6 | 	stepBase: number;
 7 | 	mattingSource: ImageBitmap;
 8 | 	/** 是否为擦除画笔 */
 9 | 	isErasing?: boolean;
10 | }
11 | 
12 | export interface ComputedMovements {
13 | 	unsignedMovementX: number;
14 | 	unsignedMovementY: number;
15 | 	maxMovement: number;
16 | }
17 | 
18 | /** 处理插值的绘制点的配置对象 */
19 | export interface RenderInterpolationConfig {
20 | 	/** 绘制点的配置对象 */
21 | 	drawingConfig: MattingDrawingConfig;
22 | 	/** 无符号水平移动距离 */
23 | 	unsignedMovementX: number;
24 | 	/** 无符号水平移动距离 */
25 | 	unsignedMovementY: number;
26 | 	/** 无符号水平/垂直移动距离较大的那个 */
27 | 	maxMovement: number;
28 | }
29 | 
30 | /** 判断是否在图像范围内 */
31 | export type InImageRangeConfig = PositionRange & PixelPosition;
32 | 
33 | /** 插值步长 */
34 | export interface InterpolationStep {
35 | 	stepX: number;
36 | 	stepY: number;
37 | }
38 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/listener-manager.d.ts:
--------------------------------------------------------------------------------
 1 | export interface MouseListenerContext {
 2 | 	/** 触发鼠标事件的目标DOM */
 3 | 	mouseTarget: HTMLElement;
 4 | 	/** mousemove监听器 */
 5 | 	move: (ev: MouseEvent) => void;
 6 | 	/** mousedown监听器 */
 7 | 	down?: (ev: MouseEvent) => void | boolean;
 8 | 	/** mouseup监听器 */
 9 | 	up?: (ev: MouseEvent) => void;
10 | }
11 | 
12 | /** 用于解绑mousedown监听器、mouseup监听器的回调的配置对象 */
13 | export interface UnbindDownUpConfig {
14 | 	/** 解绑mousedown监听器的回调 */
15 | 	unbindDown: VoidFunction;
16 | 	/** 解绑mouseup监听器的回调 */
17 | 	unbindUp: VoidFunction;
18 | }
19 | 
20 | /** 事件监听配置对象 */
21 | export interface ListenerConfig {
22 | 	/** 事件类型 */
23 | 	eventType: string;
24 | 	/** 事件监听器 */
25 | 	listener: EventListener;
26 | 	stop?: boolean;
27 | 	prevent?: boolean;
28 | }
29 | 
30 | export interface WheelListenerContext {
31 | 	/** 输入端(左侧)画板 */
32 | 	mattingBoards: HTMLCanvasElement[];
33 | 	/** 滑动开始的监听器 */
34 | 	wheel: (ev: WheelEvent) => void;
35 | }
36 | 
37 | /** 存放UnbindDownUpConfig对象的容器 */
38 | export type UnbindDownUpCache = WeakMap<HTMLElement, UnbindDownUpConfig>;
39 | 
40 | /** 存放解绑mousemove监听器的回调的容器 */
41 | export type UnbindMoveCache = WeakMap<HTMLElement, VoidFunction>;
42 | 
43 | /** 解绑Wheel监听器的回调的容器 */
44 | export type UnbindWheelCache = Set<VoidFunction>;
45 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/matting-drawing.d.ts:
--------------------------------------------------------------------------------
1 | export type GLColor = [number, number, number, number];
2 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/matting.d.ts:
--------------------------------------------------------------------------------
 1 | export interface MattingType {
 2 |   value: {}
 3 |   /** 是否为擦除画笔 */
 4 |   isErasing: boolean
 5 |   /** 下载结果图 */
 6 |   onDownloadResult: Function
 7 |   /** 返回结果图 */
 8 |   getResult: Function
 9 |   /** input表单选择文件的回调 */
10 |   onFileChange: Function
11 |   /**
12 |    * 初始化加载的图片,第一个参数为原始图像,第二个参数为裁剪图像
13 |    */
14 |   initLoadImages: Function
15 |   /** 画笔尺寸 */
16 |   radius: number | string
17 |   /** 画笔尺寸:计算属性,显示值 */
18 |   brushSize: any
19 |   /** 画笔硬度 */
20 |   hardness: number | string
21 |   /** 画笔硬度:计算属性,显示值 */
22 |   hardnessText: any
23 |   /** 常量 */
24 |   constants: {
25 |     RADIUS_SLIDER_MIN: number
26 |     RADIUS_SLIDER_MAX: number
27 |     RADIUS_SLIDER_STEP: number
28 |     HARDNESS_SLIDER_MAX: number
29 |     HARDNESS_SLIDER_STEP: number
30 |     HARDNESS_SLIDER_MIN: number
31 |   }
32 | }
33 | 


--------------------------------------------------------------------------------
/packages/image-extraction/types/transform.d.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-05 16:33:07
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-08 11:11:01
 7 |  */
 8 | import ListenerManager from '../helpers/listener-manager'
 9 | import { InitTransformedDrawBaseConfig, PositionRange } from './common'
10 | import { DirectlyDrawingContext } from './dom'
11 | 
12 | export interface InitMattingTransformConfig extends InitTransformedDrawBaseConfig {
13 |   /** 输入画板变换时绘制的上下文对象 */
14 |   inputContexts: DirectlyDrawingContext
15 |   /** 输出画板变换时绘制的上下文对象 */
16 |   outputContexts: DirectlyDrawingContext
17 | }
18 | 
19 | export interface InitMattingScaleConfig extends InitMattingTransformConfig {
20 |   listenerManager: ListenerManager
21 | }
22 | 
23 | /** 初始化抠图画板变化的配置对象 */
24 | export interface InitMattingDragConfig extends InitMattingScaleConfig {
25 |   /** 是否正在拖动左侧输入区画板 */
26 |   draggingInputBoard: boolean
27 | }
28 | 
29 | /** 生成绘制返回偏移量的配置对象 */
30 | export interface GenerateRangeOffsetConfig {
31 |   pageX: number
32 |   pageY: number
33 |   positionRange: PositionRange
34 | }
35 | 


--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   plugins: {
 3 |     autoprefixer: {
 4 |       overrideBrowserslist: [
 5 |         'Android 4.1',
 6 |         'iOS 7.1',
 7 |         'Chrome > 31',
 8 |         'ff > 31',
 9 |         'ie >= 8',
10 |         'last 10 versions', // 所有主流浏览器最近10版本用
11 |       ],
12 |       grid: false,
13 |     },
14 |   },
15 | }
16 | 


--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 | <svg t="1647329483504" class="icon" viewBox="0 0 1260 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4962" width="32" height="32" data-spm-anchor-id="a313x.7781069.0.i13"><path d="M166.137 564.657a35.604 35.604 0 1 1 71.168 0 35.604 35.604 0 0 1-71.168 0z m54.745-179.83a55.847 55.847 0 1 1 111.695 0 55.847 55.847 0 0 1-111.656 0z m206.297-146.314a72.074 72.074 0 1 1 144.108 0 72.074 72.074 0 0 1-144.108 0z m272.541 0a90.466 90.466 0 1 1 180.972 0 90.466 90.466 0 0 1-180.972 0z m549.022-88.103a31.783 31.783 0 0 1 3.15 44.82L771.597 748.308l-108.74 75.658 60.612-119.139 479.39-552.093a31.783 31.783 0 0 1 44.859-3.151l0.945 0.787z m-82.157 307.003a32.414 32.414 0 0 1 32.296 34.973h0.118c-2.56 29.46-73.059 298.536-269.982 415.35-328.192 194.718-543.35 48.759-586.358-20.755-29.696-48.01-52.381-100.864-69.71-120.793-27.215-31.192-150.056 43.245-233.945-50.845-83.929-94.13-58.526-470.45 343.276-651.028 367.931-165.337 631.414 36.155 673.162 71.286a31.429 31.429 0 1 1-38.99 49.35C917.241 102.4 719.53 28.198 487.357 89.796c-331.815 86.41-465.683 435.554-408.97 570.25 30.523 72.467 175.853 6.223 233.315 53.996 21.425 17.802 59.037 91.254 83.732 134.774 44.898 79.281 286.72 140.997 497.979 3.82 195.82-127.133 240.718-365.331 240.718-365.331h0.119a32.414 32.414 0 0 1 32.295-29.893z" fill="#1195db" p-id="4963" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"></path></svg>


--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /


--------------------------------------------------------------------------------
/service/.gitignore:
--------------------------------------------------------------------------------
 1 | # 创建自己的业务配置,不提交到库
 2 | # config.*
 3 | # Logs
 4 | logs
 5 | *.log
 6 | npm-debug.log*
 7 | yarn-debug.log*
 8 | yarn-error.log*
 9 | .DS_Store
10 | **/.DS_Store
11 | static
12 | config.json
13 | config.js
14 | yarn.lock
15 | 
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 | 
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 | 
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | 
28 | # nyc test coverage
29 | .nyc_output
30 | 
31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 | 
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 | 
37 | # node-waf configuration
38 | .lock-wscript
39 | 
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 | 
43 | # Dependency directories
44 | node_modules/
45 | dist/
46 | jspm_packages/
47 | _apidoc/
48 | 
49 | # TypeScript v1 declaration files
50 | typings/
51 | 
52 | # Optional npm cache directory
53 | .npm
54 | 
55 | # Optional eslint cache
56 | .eslintcache
57 | 
58 | # Optional REPL history
59 | .node_repl_history
60 | 
61 | # Output of 'npm pack'
62 | *.tgz
63 | 
64 | # Yarn Integrity file
65 | .yarn-integrity
66 | 
67 | # dotenv environment variables file
68 | .env
69 | 
70 | # next.js build output
71 | .next
72 | 


--------------------------------------------------------------------------------
/service/.vscode/api-get.code-snippets:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"Print to console": {
 3 | 		"prefix": "api-get",
 4 | 		"body": [
 5 | 			"/**",
 6 | 			"* @api {get} ${0:__接口url__} ${1:__接口说明__}",
 7 | 			"* @apiVersion 1.0.0",
 8 | 			"* @apiGroup ${2:__分组__}",
 9 | 			"* @apiDescription ${3:__详细描述__}",
10 | 			"* ",
11 | 			"* @apiUse needToken",
12 | 			"* @apiHeader {__类型__} __字段名__ __头字段说明__",
13 | 			"* ",
14 | 			"* @apiParam {__类型__} __字段名__=__默认值__ __请求字段说明__",
15 | 			"* ",
16 | 			"* @apiSuccess (__组__) {__类型__} __字段名__ __返回字段说明__",
17 | 			"* ", 
18 | 			"* @apiUse __SuccessName定义的成功返回块__",
19 | 			"*/"
20 | 		],
21 | 		"description": "GET请求 - apidoc文档模板"
22 | 	}
23 | }


--------------------------------------------------------------------------------
/service/.vscode/api-graphql.code-snippets:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"Print to console": {
 3 | 		"prefix": "api-graphql",
 4 | 		"body": [
 5 | 			"/**",
 6 | 			"* @api {get} graphql ${1:__接口说明__}",
 7 | 			"* @apiName ${2: __方法名称__}",
 8 | 			"* @apiVersion 1.0.0",
 9 | 			"* @apiGroup ${3:__分组_gql__}",
10 | 			"* @apiDescription ${4:__详细描述__}",
11 | 			"* @apiSampleRequest off",
12 | 			"* ",
13 | 			"* @apiParam {String} token 用户验签",
14 | 			"* @apiParam {__类型__} __字段名__=__默认值__ __请求字段说明__",
15 | 			"* ",
16 | 			"* @apiSuccess (__组__) {__类型__} __字段名__ __返回字段说明__",
17 | 			"* ", 
18 | 			"* @apiUse __定义的可选参数模块名MODEL__",
19 | 			"*/",
20 | 			"/**",
21 | 			"* ----copy---- ",
22 | 			"* @apiDefine __定义的模块名MODEL__",
23 | 			"* @apiParam (可选返回参数) {__类型__} __参数__ __说明__",
24 | 			"*/",
25 | 		],
26 | 		"description": "Graphql-GET请求 - apidoc文档模板"
27 | 	}
28 | }


--------------------------------------------------------------------------------
/service/.vscode/api-post.code-snippets:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"Print to console": {
 3 | 		"prefix": "api-post",
 4 | 		"body": [
 5 | 			"/**",
 6 | 			"* @api {post} ${0:__接口url__} ${1:__接口说明__}",
 7 | 			"* @apiVersion 1.0.0",
 8 | 			"* @apiGroup ${2:__分组__}",
 9 | 			"* @apiDescription ${3:__详细描述__}",
10 | 			"* ",
11 | 			"* @apiUse needToken",
12 | 			"* @apiHeader {__类型__} __字段名__ __头字段说明__",
13 | 			"* ",
14 | 			"* @apiParam {__类型__} __字段名__=__默认值__ __请求字段说明__",
15 | 			"* ",
16 | 			"* @apiSuccess (__组__) {__类型__} __字段名__ __返回字段说明__",
17 | 			"* ", 
18 | 			"* @apiUse __SuccessName定义的成功返回块__",
19 | 			"*/"
20 | 		],
21 | 		"description": "POST请求 - apidoc文档模板"
22 | 	}
23 | }


--------------------------------------------------------------------------------
/service/.vscode/api-success.code-snippets:
--------------------------------------------------------------------------------
 1 | {
 2 | 	"Print to console": {
 3 | 		"prefix": "api-success",
 4 | 		"body": [
 5 | 			"/**",
 6 | 			"* @apiDefine ${1:__定义模板名称__}",
 7 | 			"* @apiSuccessExample {json} Success-Response:",
 8 | 			"*     HTTP/1.1 200 OK",
 9 | 			"*     {",
10 | 			"*       'message': 'ok'",
11 | 			"*     }",
12 | 			"*/",
13 | 		],
14 | 		"description": "成功返回例子 - apidoc文档模板"
15 | 	}
16 | }


--------------------------------------------------------------------------------
/service/.vscode/apidoc.code-snippets:
--------------------------------------------------------------------------------
 1 | {
 2 | 	// ${1:label}, ${2:another}
 3 | 	"Print to console": {
 4 | 		"prefix": "apidoc",
 5 | 		"body": [
 6 | 			"/**",
 7 | 			"* @api {get} /user/getUserById/:id ${3:__apiname__}",
 8 | 			"* @apiGroup ${1:label}",
 9 | 			"*/"
10 | 		],
11 | 		"description": "my ts-vue template"
12 | 	}
13 | }


--------------------------------------------------------------------------------
/service/README.md:
--------------------------------------------------------------------------------
 1 | ## Node截图服务
 2 | 
 3 | 项目中所使用到的图片生成接口为:`api/screenshots`,在真实生产项目中可以把该服务单独部署,于内网调用,这样利于做一些鉴权之类的处理。
 4 | 
 5 | 注:另外的 `api/printscreen` 本项目中并未使用,这个接口的作用是实现普通网页截图,可以传入一个 URL 生成该网址的预览图片,用于合成长图分享海报等场景。
 6 | 
 7 | ### 安装依赖
 8 | 
 9 | `npm install`
10 | 
11 | 安装依赖时可能会出现这个报错提示:
12 | 
13 | ```
14 | ERROR: Failed to set up Chromium xxx! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.
15 | ```
16 | 
17 | 不用慌,这是因为 puppeteer 会自动下载 Chromium,国内可能受到网络波动的影响而失败。
18 | 
19 | 如果跳过的话需要手动安装,比较麻烦所以并不推荐,请**多尝试安装几次,或者更换国内的镜像源再安装**。
20 | 
21 | ### 启动项目并热更新
22 | 
23 | `npm run dev`
24 | 
25 | ### 打包
26 | 
27 | `npm run build`
28 | 
29 | #### 打包部署步骤
30 | 
31 | > 服务器环境需求:
32 | > 
33 | > - Node.js 16.18.1(尽量保持生产版本相同,避免出现错误)
34 | > 
35 | > - PM2(进程守护)
36 | 
37 | 1. 本地执行 `npm run build` 打包
38 | 2. 打包后项目根目录 `dist/` 文件夹上传服务器,并执行 `npm install` 安装依赖
39 | 3. 运行 `pm2 start dist/server.js` 启动并守护服务
40 | 
41 | ### 配置说明
42 | 
43 | 配置文件 `src/config.ts` 配置项说明:
44 | 
45 | ```js
46 | port // 端口号
47 | website // 编辑器项目的地址
48 | filePath // 生成图片保存的目录
49 | ```
50 | 
51 | ### 多线程集群
52 | 
53 | 本服务中实现多任务操作使用的是队列的处理方式,保留了 JavaScript 单线程的特点,线程安全并且性能高效,能够保证下限更稳定,但在高配置机器上可能无法充分利用多核 CPU 资源。
54 | 
55 | 如果你希望在配置更高的机器上创建多线程集群,可以尝试使用 [puppeteer-cluster](https://github.com/thomasdondorf/puppeteer-cluster)。
56 | 
57 | ### 生成 API 文档
58 | 
59 | `build:apidoc`


--------------------------------------------------------------------------------
/service/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "screenshot-node",
 3 |   "version": "1.0.0",
 4 |   "description": "",
 5 |   "main": "./src/main",
 6 |   "scripts": {
 7 |     "dev": "cross-env NODE_ENV=development ts-node-dev src/main",
 8 |     "build": "cross-env NODE_ENV=production webpack",
 9 |     "serve": "ts-node src/main",
10 |     "start": "webpack --watch",
11 |     "serverstart": "pm2 start ./dist/server.js --watch",
12 |     "tscstart": "tsc -w",
13 |     "build:apidoc": "apidoc -i src/ -o _apidoc/",
14 |     "publish": "rm -rf ./static && rm -rf ./_apidoc && sh script/publish.sh",
15 |     "publish-fast": "git add . && git commit -m 'build: auto publish' && npm run publish"
16 |   },
17 |   "author": "ShawnPhang",
18 |   "license": "ISC",
19 |   "dependencies": {
20 |     "axios": "^1.7.3",
21 |     "body-parser": "^1.19.0",
22 |     "express": "^4.19.2",
23 |     "image-size": "^1.1.1",
24 |     "images": "^3.2.4",
25 |     "multiparty": "^4.2.3",
26 |     "puppeteer": "^10.4.0"
27 |   },
28 |   "devDependencies": {
29 |     "@types/express": "^4.17.21",
30 |     "@types/multiparty": "^4.2.1",
31 |     "@types/node": "^16.18.105",
32 |     "cross-env": "^7.0.3",
33 |     "ts-loader": "^6.0.4",
34 |     "ts-node": "^8.3.0",
35 |     "ts-node-dev": "^2.0.0",
36 |     "typescript": "^3.5.3",
37 |     "webpack": "^5.90.3",
38 |     "webpack-cli": "^5.1.4",
39 |     "webpack-node-externals": "^3.0.0"
40 |   },
41 |   "apidoc": {
42 |     "title": "自动api接口文档",
43 |     "url": "http://localhost:9999/",
44 |     "sampleUrl": "http://localhost:9999/"
45 |   }
46 | }
47 | 


--------------------------------------------------------------------------------
/service/src/configs.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-01 13:41:59
 4 |  * @Description: 配置文件
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 05:13:19
 7 |  */
 8 | const isDev = process.env.NODE_ENV === 'development'
 9 | 
10 | // 服务器常用修改项
11 | const serviceComfig = {
12 |     port: 7001, // 端口号
13 |     website: 'http://127.0.0.1:5173/', // 编辑器项目的地址
14 |     filePath: '/cache/' // 生成图片保存的目录
15 | }
16 | 
17 | /**
18 |  * 端口号
19 |  */
20 | export const servicePort = serviceComfig.port
21 | 
22 | /**
23 |  * 前端绘制页地址
24 |  */
25 | export const drawLink = isDev ? 'http://127.0.0.1:5173/draw' : serviceComfig.website + '/draw'
26 | 
27 | /**
28 |  * 图片缓存目录位置,根据实际情况调整
29 |  */
30 | export const filePath = isDev ? process.cwd() + `/static/` : serviceComfig.filePath
31 | 
32 | /**
33 |  * 配置服务器端的chrome浏览器位置
34 |  */
35 | export const executablePath = isDev ? null : '/opt/google/chrome-unstable/chrome'
36 | 
37 | /**
38 |  * 截图并发数上限
39 |  */
40 | export const maxNum = 2
41 | 
42 | /**
43 |  * 截图队列的阈值,超出时请求将会被熔断
44 |  */
45 | export const upperLimit = 20
46 | 
47 | /**
48 |  * 多久释放浏览器驻留内存,单位:秒(多标签页版生效)
49 |  */
50 | export const releaseTime = 300
51 | 


--------------------------------------------------------------------------------
/service/src/control/api.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2020-07-22 20:13:14
 4 |  * @Description: 接口名称
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 13:39:59
 7 |  */
 8 | let path = '/api'
 9 | 
10 | export default {
11 |   SCREENGHOT: path + '/screenshots',
12 |   PRINTSCREEN: path + '/printscreen',
13 |   // 后端示例
14 |   UPLOAD: path + '/file/upload',
15 |   USER_IMAGES: '/design/user/image',
16 |   GET_TEMPLATE_LIST: '/design/list',
17 |   GET_TEMPLATE: '/design/temp',
18 |   GET_MATERIAL: '/design/material',
19 |   GET_PHOTOS: '/design/imgs',
20 |   UPDATE_TEMPLATE: '/design/edit',
21 | }


--------------------------------------------------------------------------------
/service/src/control/router.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2020-07-22 20:13:14
 4 |  * @Description: 路由
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 13:40:13
 7 |  */
 8 | import rExpress from 'express'
 9 | import screenshots from '../service/screenshots'
10 | import fileService from '../service/files'
11 | import userService from '../service/user'
12 | import designService from '../service/design'
13 | import api from './api'
14 | const rRouter = rExpress.Router()
15 | 
16 | rRouter.get(api.SCREENGHOT, screenshots.screenshots)
17 | rRouter.get(api.PRINTSCREEN, screenshots.printscreen)
18 | rRouter.post(api.UPLOAD, fileService.upload)
19 | rRouter.get(api.USER_IMAGES, userService.getUserImages)
20 | rRouter.get(api.GET_TEMPLATE_LIST, designService.getTemplates)
21 | rRouter.get(api.GET_TEMPLATE, designService.getDetail)
22 | rRouter.get(api.GET_MATERIAL, designService.getMaterial)
23 | rRouter.get(api.GET_PHOTOS, designService.getPhotos)
24 | rRouter.post(api.UPDATE_TEMPLATE, designService.saveTemplate)
25 | 
26 | export default rRouter
27 | 


--------------------------------------------------------------------------------
/service/src/main.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-01 13:41:59
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-11-14 17:36:17
 7 |  */
 8 | 
 9 | import express from 'express'
10 | import bodyParser from 'body-parser'
11 | import fs from 'fs'
12 | import router from './control/router'
13 | import { filePath, servicePort } from './configs'
14 | import handleTimeout from './utils/timeout'
15 | 
16 | const port = process.env.PORT || servicePort
17 | const app = express()
18 | 
19 | // 创建目录
20 | const createFolder = (folder: string) => {
21 |   try {
22 |     fs.accessSync(folder)
23 |   } catch (e) {
24 |     fs.mkdirSync(folder)
25 |   }
26 | }
27 | createFolder(filePath)
28 | 
29 | app.all('*', (req: any, res: any, next: any) => {
30 |   res.header('Access-Control-Allow-Origin', '*')
31 |   res.header('Access-Control-Allow-Headers', 'X-Access-Token,Content-Type,Authorization,Content-Length,Content-Size')
32 |   res.header('Access-Control-Allow-Methods', '*')
33 |   res.header('Content-Type', 'application/json;charset=utf-8')
34 |   next()
35 | })
36 | 
37 | app.use('/static', setUploadContentType, express.static(process.cwd() + `/static/`))
38 | if (process.env.NODE_ENV === 'development') {
39 |   app.use('/store', setUploadContentType, express.static(process.cwd() + `/src/mock/assets`))
40 | }
41 | 
42 | app.use(handleTimeout)
43 | 
44 | app.use((req: any, res: any, next: any) => {
45 |   console.log(req.path)
46 |   next()
47 | })
48 | 
49 | app.use(bodyParser.urlencoded({ limit: '100mb', extended: true, parameterLimit: 100000 }))
50 | app.use(bodyParser.json({ limit: '100mb' }))
51 | app.use(router)
52 | 
53 | app.listen(port, () => console.log(`Screenshot Server start on port:${port}`))
54 | 
55 | const getContentType = function (path: any) {
56 |   const extension = path.split('.').pop().toLowerCase()
57 |   switch (extension) {
58 |     case 'jpg':
59 |     case 'jpeg':
60 |       return 'image/jpeg'
61 |     case 'png':
62 |       return 'image/png'
63 |     case 'gif':
64 |       return 'image/gif'
65 |     case 'svg':
66 |       return 'image/svg+xml'
67 |     default:
68 |       return null
69 |   }
70 | }
71 | 
72 | function setUploadContentType(req: any, res: any, next: any) {
73 |   const contentType = getContentType(req.path)
74 |   if (contentType) {
75 |     res.setHeader('Content-Type', contentType)
76 |   }
77 |   next()
78 | }
79 | 


--------------------------------------------------------------------------------
/service/src/mock/assets/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palxiao/poster-design/68d3e7bffa36950f3740ed92744766102a910bae/service/src/mock/assets/1.png


--------------------------------------------------------------------------------
/service/src/mock/assets/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palxiao/poster-design/68d3e7bffa36950f3740ed92744766102a910bae/service/src/mock/assets/2.png


--------------------------------------------------------------------------------
/service/src/mock/cates.json:
--------------------------------------------------------------------------------
1 | [
2 |   {
3 |       "id": 1,
4 |       "name": "手机海报",
5 |       "type": 1,
6 |       "pid": null
7 |   }
8 | ]


--------------------------------------------------------------------------------
/service/src/mock/components/detail/1.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "id": 1907,
 3 |   "cover": "https://pic.imgdb.cn/item/66b96b6ad9c307b7e9568778.png",
 4 |   "data": "{\"name\":\"文本\",\"type\":\"w-text\",\"uuid\":-1,\"editable\":false,\"left\":40,\"top\":292,\"transform\":\"\",\"lineHeight\":1.2,\"letterSpacing\":0,\"fontSize\":180,\"fontClass\":{\"alias\":\"站酷快乐体\",\"id\":543,\"value\":\"zcool-kuaile-regular\",\"url\":\"https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2\"},\"fontWeight\":400,\"fontStyle\":\"normal\",\"writingMode\":\"horizontal-tb\",\"textDecoration\":\"none\",\"color\":\"#000000ff\",\"textAlign\":\"center\",\"text\":\"%E8%BE%93%E5%85%A5%E6%96%87%E5%AD%97\",\"opacity\":1,\"backgroundColor\":\"\",\"parent\":\"-1\",\"record\":{\"width\":0,\"height\":0,\"minWidth\":0,\"minHeight\":0,\"dir\":\"horizontal\"},\"width\":721,\"height\":217,\"rotate\":0,\"transformData\":{\"a\":1,\"b\":0,\"c\":0,\"d\":1,\"tx\":0,\"ty\":0},\"textEffects\":[{\"stroke\":{\"enable\":true,\"type\":\"center\",\"color\":\"#8581f7ff\",\"width\":11.122105263157893},\"offset\":{\"x\":3.7082741393523917,\"y\":6.171604565055211,\"enable\":true}},{\"stroke\":{\"enable\":true,\"type\":\"center\",\"color\":\"#8581f7ff\",\"width\":11.520000000000001}},{\"filling\":{\"enable\":true,\"type\":0,\"color\":\"#ffffffff\",\"imageContent\":{\"repeat\":0,\"scaleX\":1,\"scaleY\":1,\"image\":null,\"width\":null,\"height\":null},\"gradient\":{\"byLine\":0,\"angle\":0,\"stops\":[{\"color\":\"#ffffffff\",\"offset\":0},{\"color\":\"#000000ff\",\"offset\":1}]}}}]}",
 5 |   "created_time": "2023-11-29T11:07:49.000Z",
 6 |   "updated_time": "2023-11-29T11:13:04.000Z",
 7 |   "title": "",
 8 |   "width": null,
 9 |   "height": null,
10 |   "state": 1,
11 |   "tag": null
12 | }
13 | 


--------------------------------------------------------------------------------
/service/src/mock/components/detail/2.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "id": 1897,
 3 |   "cover": "https://pic.imgdb.cn/item/66b96b84d9c307b7e9569826.png",
 4 |   "data": "{\"name\":\"文本\",\"type\":\"w-text\",\"uuid\":-1,\"editable\":false,\"left\":46.00000000000002,\"top\":292,\"transform\":\"\",\"lineHeight\":1.2,\"letterSpacing\":0,\"fontSize\":180,\"fontClass\":{\"alias\":\"站酷快乐体\",\"id\":543,\"value\":\"zcool-kuaile-regular\",\"url\":\"https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2\"},\"fontWeight\":400,\"fontStyle\":\"normal\",\"writingMode\":\"horizontal-tb\",\"textDecoration\":\"none\",\"color\":\"#b8f5ffff\",\"textAlign\":\"center\",\"text\":\"%E8%BE%93%E5%85%A5%E6%96%87%E5%AD%97\",\"opacity\":1,\"backgroundColor\":\"\",\"parent\":\"-1\",\"record\":{\"width\":0,\"height\":0,\"minWidth\":0,\"minHeight\":0,\"dir\":\"horizontal\"},\"width\":709,\"height\":217,\"rotate\":0,\"transformData\":{\"a\":1,\"b\":0,\"c\":0,\"d\":1,\"tx\":0,\"ty\":0},\"textEffects\":[{\"stroke\":{\"color\":\"#148ef5ff\",\"type\":\"outer\",\"width\":5.538461538461537,\"enable\":true},\"filling\":{\"enable\":true,\"type\":0,\"color\":\"#b8f5ffff\",\"imageContent\":{\"repeat\":0,\"scaleX\":1,\"scaleY\":1,\"image\":null,\"width\":null,\"height\":null},\"gradient\":{\"byLine\":0,\"angle\":0,\"stops\":[{\"color\":\"#ffffffff\",\"offset\":0},{\"color\":\"#000000ff\",\"offset\":1}]}}}]}",
 5 |   "created_time": "2023-11-29T11:07:38.000Z",
 6 |   "updated_time": "2023-11-29T11:18:09.000Z",
 7 |   "title": "花字-输入文字",
 8 |   "width": null,
 9 |   "height": null,
10 |   "state": 1,
11 |   "tag": null
12 | }


--------------------------------------------------------------------------------
/service/src/mock/components/detail/3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "id": 1893,
 3 |   "cover": "https://pic.imgdb.cn/item/66b96ba4d9c307b7e956b11f.png",
 4 |   "data": "{\"name\":\"文本\",\"type\":\"w-text\",\"uuid\":-1,\"editable\":false,\"left\":40,\"top\":292,\"transform\":\"\",\"lineHeight\":1.2,\"letterSpacing\":0,\"fontSize\":180,\"fontClass\":{\"alias\":\"站酷快乐体\",\"id\":543,\"value\":\"zcool-kuaile-regular\",\"url\":\"https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2\"},\"fontWeight\":400,\"fontStyle\":\"normal\",\"writingMode\":\"horizontal-tb\",\"textDecoration\":\"none\",\"color\":\"#e5b187ff\",\"textAlign\":\"center\",\"text\":\"%E8%BE%93%E5%85%A5%E6%96%87%E5%AD%97\",\"opacity\":1,\"backgroundColor\":\"\",\"parent\":\"-1\",\"record\":{\"width\":0,\"height\":0,\"minWidth\":0,\"minHeight\":0,\"dir\":\"horizontal\"},\"width\":721,\"height\":217,\"rotate\":0,\"transformData\":{\"a\":1,\"b\":0,\"c\":0,\"d\":1,\"tx\":0,\"ty\":0},\"textEffects\":[{\"stroke\":{\"enable\":true,\"type\":\"outer\",\"color\":\"#ffffffff\",\"width\":10.080000000000002},\"filling\":{\"enable\":true,\"type\":0,\"color\":\"#e5b187ff\",\"imageContent\":{\"repeat\":0,\"scaleX\":1,\"scaleY\":1,\"image\":null,\"width\":null,\"height\":null},\"gradient\":{\"byLine\":0,\"angle\":0,\"stops\":[{\"color\":\"#ffffffff\",\"offset\":0},{\"color\":\"#000000ff\",\"offset\":1}]}},\"offset\":{\"x\":0.00045311068155588286,\"y\":0.0005595450922490192,\"enable\":true}},{\"filling\":{\"enable\":true,\"type\":2,\"color\":\"linear-gradient(90deg, #b6aff4ff 0%,#ffa6a9ff 98.83720930232558%)\",\"imageContent\":{\"repeat\":0,\"scaleX\":1,\"scaleY\":1,\"image\":null,\"width\":null,\"height\":null},\"gradient\":{\"byLine\":0,\"angle\":90,\"stops\":[{\"color\":\"#b6aff4ff\",\"offset\":0},{\"color\":\"#ffa6a9ff\",\"offset\":0.9883720930232558}]}}}]}",
 5 |   "created_time": "2023-11-29T11:07:30.000Z",
 6 |   "updated_time": "2023-11-29T11:12:58.000Z",
 7 |   "title": "",
 8 |   "width": null,
 9 |   "height": null,
10 |   "state": 1,
11 |   "tag": null
12 | }


--------------------------------------------------------------------------------
/service/src/mock/components/list/comp.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "id": 4,
 4 |     "cover": "https://pic.imgdb.cn/item/66b96bbbd9c307b7e956c11f.png",
 5 |     "title": "穿搭品牌文字组合",
 6 |     "width": 763.5,
 7 |     "height": 436.41,
 8 |     "state": 1
 9 |   },
10 |   {
11 |     "id": 5,
12 |     "cover": "https://pic.imgdb.cn/item/66b96c65d9c307b7e95738de.png",
13 |     "title": "穿搭品牌文字组合",
14 |     "width": 794,
15 |     "height": 500,
16 |     "state": 1
17 |   },
18 |   {
19 |     "id": 6,
20 |     "cover": "https://pic.imgdb.cn/item/66b96cc9d9c307b7e958455f.png",
21 |     "title": "穿搭品牌文字组合",
22 |     "width": 766.43,
23 |     "height": 366.5,
24 |     "state": 1
25 |   }
26 | ]
27 | 


--------------------------------------------------------------------------------
/service/src/mock/components/list/text.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "id": 1,
 4 |     "cover": "https://pic.imgdb.cn/item/66b96b6ad9c307b7e9568778.png",
 5 |     "title": "",
 6 |     "width": null,
 7 |     "height": null,
 8 |     "state": 1
 9 |   },
10 |   {
11 |     "id": 2,
12 |     "cover": "https://pic.imgdb.cn/item/66b96b84d9c307b7e9569826.png",
13 |     "title": "花字-输入文字",
14 |     "width": null,
15 |     "height": null,
16 |     "state": 1
17 |   },
18 |   {
19 |     "id": 3,
20 |     "cover": "https://pic.imgdb.cn/item/66b96ba4d9c307b7e956b11f.png",
21 |     "title": "",
22 |     "width": null,
23 |     "height": null,
24 |     "state": 1
25 |   }
26 | ]
27 | 


--------------------------------------------------------------------------------
/service/src/mock/materials/mask.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "id": 1,
 4 |     "title": "爱心图片容器",
 5 |     "width": 1000,
 6 |     "height": 1000,
 7 |     "type": "mask",
 8 |     "model": "{}",
 9 |     "thumb": "https://s2.loli.net/2024/08/31/qok3XjLf728Ixat.png",
10 |     "url": "https://s2.loli.net/2024/08/31/qok3XjLf728Ixat.png",
11 |     "created_time": "2023-08-20T21:46:49.000Z",
12 |     "updated_time": "2023-09-15T11:42:14.000Z",
13 |     "state": 1
14 |   },
15 |   {
16 |     "id": 2,
17 |     "title": "扇形图片容器",
18 |     "width": 1000,
19 |     "height": 1000,
20 |     "type": "mask",
21 |     "model": "{}",
22 |     "thumb": "https://s2.loli.net/2024/08/31/giTNsOpKLZHqQmz.png",
23 |     "url": "https://s2.loli.net/2024/08/31/giTNsOpKLZHqQmz.png",
24 |     "created_time": "2023-08-20T21:46:52.000Z",
25 |     "updated_time": "2023-09-15T11:42:14.000Z",
26 |     "state": 1
27 |   },
28 |   {
29 |     "id": 3,
30 |     "title": "星星图片容器",
31 |     "width": 1000,
32 |     "height": 1000,
33 |     "type": "mask",
34 |     "model": "{}",
35 |     "thumb": "https://s2.loli.net/2024/08/31/LRt6aJpsnebKZ2X.png",
36 |     "url": "https://s2.loli.net/2024/08/31/LRt6aJpsnebKZ2X.png",
37 |     "created_time": "2023-08-20T21:47:03.000Z",
38 |     "updated_time": "2023-09-15T11:42:14.000Z",
39 |     "state": 1
40 |   }
41 | ]
42 | 


--------------------------------------------------------------------------------
/service/src/mock/materials/png.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |     "id": 886,
 4 |     "title": "卡通-计划便利贴元素",
 5 |     "width": 800,
 6 |     "height": 800,
 7 |     "type": "image",
 8 |     "model": "{}",
 9 |     "thumb": "https://pic.imgdb.cn/item/66b96dddd9c307b7e95c2db6.png",
10 |     "url": "https://pic.imgdb.cn/item/66b96dddd9c307b7e95c2db6.png",
11 |     "created_time": "2023-08-20T21:46:13.000Z",
12 |     "updated_time": "2023-09-15T11:42:14.000Z",
13 |     "state": 1
14 |   },
15 |   {
16 |     "id": 885,
17 |     "title": "卡通-计划便利贴元素",
18 |     "width": 800,
19 |     "height": 800,
20 |     "type": "image",
21 |     "model": "{}",
22 |     "thumb": "https://pic.imgdb.cn/item/66b96df2d9c307b7e95c8109.png",
23 |     "url": "https://pic.imgdb.cn/item/66b96df2d9c307b7e95c8109.png",
24 |     "created_time": "2023-08-20T21:46:12.000Z",
25 |     "updated_time": "2023-09-15T11:42:14.000Z",
26 |     "state": 1
27 |   },
28 |   {
29 |     "id": 882,
30 |     "title": "卡通-计划便利贴元素",
31 |     "width": 800,
32 |     "height": 800,
33 |     "type": "image",
34 |     "model": "{}",
35 |     "thumb": "https://pic.imgdb.cn/item/66b96e05d9c307b7e95cc86f.png",
36 |     "url": "https://pic.imgdb.cn/item/66b96e05d9c307b7e95cc86f.png",
37 |     "created_time": "2023-08-20T21:46:07.000Z",
38 |     "updated_time": "2023-09-15T11:42:14.000Z",
39 |     "state": 1
40 |   }
41 | ]
42 | 


--------------------------------------------------------------------------------
/service/src/mock/materials/svg.json:
--------------------------------------------------------------------------------
 1 | [
 2 |   {
 3 |       "id": 996,
 4 |       "title": "SVG-基础形状-多边形",
 5 |       "width": 747.9,
 6 |       "height": 747.9,
 7 |       "type": "svg",
 8 |       "model": "{\"colors\":[\"#B59683\"]}",
 9 |       "thumb": "https://pic.imgdb.cn/item/66b96ee7d9c307b7e9604257.png",
10 |       "url": "<svg xmlns=\"http://www.w3.org/2000/svg\" data-name=\"图层 1\" viewBox=\"0 0 747.9 747.9\" preserveAspectRatio=\"none\"><path d=\"M747.8 373.9c0 30.9-60.2 53.5-67.8 82s32.7 79.2 17.8 105-78.6 15.7-99.9 37-10.8 84.7-37 99.9-75.5-25.7-105-17.8-51.1 67.8-82 67.8-53.5-60.2-82-67.8-79.2 32.7-105 17.8-15.7-78.6-37-99.9-84.7-10.8-99.9-37 25.7-75.5 17.8-105S0 404.8 0 373.9s60.2-53.5 67.8-82-32.7-79.2-17.8-105 78.6-15.7 99.9-37 10.8-84.7 37-99.9 75.5 25.7 105 17.8S343 0 373.9 0s53.5 60.2 82 67.8 79.2-32.7 105-17.8 15.7 78.6 37 99.9 84.7 10.8 99.9 37-25.7 75.5-17.8 105 67.8 51.1 67.8 82z\" fill=\"{{colors[0]}}\"/></svg>",
11 |       "created_time": "2023-08-20T21:51:23.000Z",
12 |       "updated_time": "2023-09-15T11:42:14.000Z",
13 |       "state": 1
14 |   },
15 |   {
16 |       "id": 994,
17 |       "title": "SVG-基础形状-三角形",
18 |       "width": 771.2,
19 |       "height": 682.2,
20 |       "type": "svg",
21 |       "model": "{\"colors\":[\"#F3CFD3\"]}",
22 |       "thumb": "https://pic.imgdb.cn/item/66b97267d9c307b7e962f070.png",
23 |       "url": "<svg xmlns=\"http://www.w3.org/2000/svg\" data-name=\"图层 1\" viewBox=\"0 0 771.2 682.2\" preserveAspectRatio=\"none\"><path d=\"M338.9 27L7.3 601.3c-20.7 35.9 5.2 80.9 46.8 80.9h663.1c41.5 0 67.5-45 46.7-80.9L432.3 27c-20.7-36-72.6-36-93.4 0z\" fill=\"{{colors[0]}}\"/></svg>",
24 |       "created_time": "2023-08-20T21:51:21.000Z",
25 |       "updated_time": "2023-09-15T11:42:14.000Z",
26 |       "state": 1
27 |   },
28 |   {
29 |       "id": 995,
30 |       "title": "装饰图形圆形条",
31 |       "width": 798.92,
32 |       "height": 161,
33 |       "type": "svg",
34 |       "model": "{\"colors\":[\"#b8a8ffff\"]}",
35 |       "thumb": "https://pic.imgdb.cn/item/66b9727dd9c307b7e96303ae.png",
36 |       "url": "<svg xmlns=\"http://www.w3.org/2000/svg\" data-name=\"图层 1\" viewBox=\"0 0 1052.3 212.87\"><path d=\"M945.86 0H106.43a106.44 106.44 0 0 0 0 212.87h839.43a106.44 106.44 0 1 0 0-212.87z\" fill=\"{{colors[0]}}\"/></svg>",
37 |       "created_time": "2023-08-20T21:51:21.000Z",
38 |       "updated_time": "2023-09-15T11:42:14.000Z",
39 |       "state": 1
40 |   }
41 | ]


--------------------------------------------------------------------------------
/service/src/mock/templates/list.json:
--------------------------------------------------------------------------------
1 | [
2 |   { "id": 1, "cover": "https://pic.imgdb.cn/item/66b93ff9d9c307b7e92cbb2b.jpg", "title": "示例模板1", "width": 1242, "height": 2208, "state": 1 },
3 |   { "id": 2, "cover": "https://pic.imgdb.cn/item/66b93f95d9c307b7e92c86aa.webp", "title": "示例模板2", "width": 1242, "height": 2208, "state": 1 }
4 | ]
5 | 


--------------------------------------------------------------------------------
/service/src/service/files.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-16 18:25:10
 4 |  * @Description: 文件操作示例代码,仅供参考
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 18:52:18
 7 |  */
 8 | import { Request, Response } from 'express'
 9 | const multiparty = require('multiparty')
10 | const { filePath } = require('../configs.ts')
11 | const { checkCreateFolder, randomCode, copyFile, send } = require('../utils/tools.ts')
12 | 
13 | const FileUrl = 'http://localhost:7001/static/'
14 | 
15 | 
16 | // api/file/upload 上传接口
17 | export async function upload(req: Request, res: Response) {
18 |   /**
19 |    * @api {post} /api/file/upload 上传接口
20 |    * @apiVersion 1.0.0
21 |    * @apiGroup file
22 |    *
23 |    * @apiParam {File} file 二进制文件
24 |    * @apiParam {String} folder 目标文件夹,空为根目录
25 |    * @apiParam {String} name 文件名,默认随机
26 |    *
27 |    * @apiSuccess (__组__) {__类型__} __字段名__ __返回字段说明__
28 |    */
29 |   const form = new multiparty.Form()
30 |   form.parse(req, async function (err: any, fields: any, files: any) {
31 |     if (err) {
32 |       console.error('上传文件出错!')
33 |       return
34 |     }
35 |     if (files) {
36 |       const file = files.file ? files.file[0] : {}
37 |       const { size, headers, originalFilename } = file
38 |       const fileType = headers['content-type'].split('/')[1]
39 |       const Suffix = originalFilename.split('.').pop() || fileType || 'png'
40 |       const { folder = '', name = `${randomCode(12)}.${Suffix}` } = fields
41 |       const folderPath = `${filePath}${folder ? `${folder}/` : ''}`
42 |       checkCreateFolder(folderPath) // 检测对应目录是否存在
43 |       const targetPath = `${folderPath}${name}`
44 |       copyFile(file.path, targetPath)
45 |         .then(() => {
46 |           const url = `${FileUrl}${folder ? folder + '/' : ''}${name}`
47 |           send.success(res, {
48 |             key: `${folder}/${name}`,
49 |             url,
50 |           })
51 |         })
52 |         .catch((err: any) => {
53 |           console.log('上传异常', err)
54 |         })
55 |     }
56 |   })
57 | }
58 | 
59 | export default { upload }
60 | 


--------------------------------------------------------------------------------
/service/src/service/user.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-16 18:25:10
 4 |  * @Description: 示例代码,仅供参考
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 06:25:23
 7 |  */
 8 | import { Request, Response } from 'express'
 9 | const multiparty = require('multiparty')
10 | const { filePath } = require('../configs.ts')
11 | const { checkCreateFolder, filesReader, send } = require('../utils/tools.ts')
12 | 
13 | const FileUrl = 'http://localhost:7001/static/'
14 | 
15 | export default {
16 |   // design/user/image 获取用户上传列表(虚拟)
17 |   async getUserImages(req: Request, res: Response) {
18 |     /**
19 |      * @api {post} /design/user/image 获取用户上传列表(虚拟)
20 |      * @apiVersion 1.0.0
21 |      * @apiGroup user
22 |      */
23 |     const list = await filesReader('user')
24 |     send.success(res, { list })
25 |   },
26 | }
27 | 


--------------------------------------------------------------------------------
/service/src/shims-my.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Type {
2 |     export interface Object {
3 |         [propName: string]: any
4 |     }
5 | }
6 | 
7 | 


--------------------------------------------------------------------------------
/service/src/utils/fs.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-28 17:10:51
 4 |  * @Description: 文件操作相关方法
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 06:29:58
 7 |  */
 8 | import fs from 'fs'
 9 | import path from 'path'
10 | import imageSize from 'image-size'
11 | import { filePath as StaticPath } from '../configs'
12 | const FileUrl = 'http://localhost:7001/static/'
13 | 
14 | export function copyFile(sourceFile: string, destinationFile: string): Promise<void> {
15 |   return new Promise((resolve, reject) => {
16 |     const readStream = fs.createReadStream(sourceFile)
17 |     const writeStream = fs.createWriteStream(destinationFile)
18 | 
19 |     readStream.on('error', (err: any) => {
20 |       reject(err)
21 |     })
22 | 
23 |     writeStream.on('error', (err: any) => {
24 |       reject(err)
25 |     })
26 | 
27 |     writeStream.on('finish', () => {
28 |       resolve()
29 |     })
30 | 
31 |     readStream.pipe(writeStream)
32 |   })
33 | }
34 | 
35 | // 读取目录
36 | export function filesReader(directoryPath: string) {
37 |   return new Promise((resolve) => {
38 |     try {
39 |       const files = fs.readdirSync(StaticPath + directoryPath)
40 |       const filesArray: any = []
41 |       files.forEach((file) => {
42 |         const filePath = path.join(directoryPath, file)
43 |         // const stats = fs.statSync(filePath);
44 |         const { width, height } = imageSize(StaticPath + filePath)
45 |         if (file !== '.DS_Store') {
46 |           const fileInfo = {
47 |             width,
48 |             height,
49 |             // filename: file,
50 |             // link: FileUrl + directoryPath,
51 |             url: `${FileUrl + directoryPath}/${file}`,
52 |             // filepath: StaticPath + filePath
53 |             // size: stats.size, // 文件大小
54 |             // modified: stats.mtime // 最后修改时间
55 |           }
56 |           filesArray.push(fileInfo)
57 |         }
58 |       })
59 |       // JSON.stringify(filesArray, null, 2)
60 |       resolve(filesArray)
61 |     } catch (err) {
62 |       console.error('Error reading directory:', err)
63 |     }
64 |   })
65 | }
66 | 
67 | // 读取文件
68 | export function readFile(directoryPath: string) {
69 |   return new Promise((resolve) => {
70 |     try {
71 |       resolve(fs.readFileSync(StaticPath + directoryPath, 'utf8'))
72 |     } catch (err) {
73 |       console.error('Error reading file:', err)
74 |     }
75 |   })
76 | }
77 | 
78 | export default {
79 |   copyFile,
80 |   filesReader,
81 |   readFile,
82 | }


--------------------------------------------------------------------------------
/service/src/utils/http.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-01-25 17:02:02
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 13:59:34
 7 |  */
 8 | import axios from 'axios'
 9 | 
10 | const httpRequest = axios.create({
11 |   maxContentLength: Infinity,
12 |   maxBodyLength: Infinity,
13 | })
14 | 
15 | httpRequest.interceptors.response.use((config: any) => {
16 |   return config.data
17 | })
18 | 
19 | export default httpRequest
20 | 


--------------------------------------------------------------------------------
/service/src/utils/node-queue.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-12-24 18:09:35
 4 |  * @Description: 异步队列
 5 |  * @LastEditors: ShawnPhang <site: m.palxp.cn>
 6 |  * @LastEditTime: 2023-07-06 10:19:40
 7 |  */
 8 | interface Queue {
 9 |   Fn: Function
10 |   sign?: string | number
11 | }
12 | 
13 | import { maxNum } from '../configs'
14 | const queueList: any = [] // 任务队列
15 | let curNum = 0 // 当前执行的任务数
16 | 
17 | function queueRun(business: Function, ...arg: any) {
18 |   return new Promise(async (resolve) => {
19 |     const Fn = async () => resolve(await business(...arg))
20 |     const sign = { ...arg }[2]
21 |     if (curNum >= maxNum) {
22 |       queueList.push({ sign, Fn })
23 |     } else {
24 |       await run(Fn)
25 |     }
26 |   })
27 | }
28 | 
29 | function run(Fn: Function) {
30 |   curNum++
31 |   Fn().then((res: any) => {
32 |     curNum--
33 |     if (queueList.length > 0) {
34 |       const Task: Queue = queueList.shift()
35 |       run(Task.Fn)
36 |     }
37 |     return res
38 |   })
39 | }
40 | 
41 | export { queueRun, queueList }
42 | 


--------------------------------------------------------------------------------
/service/src/utils/timeout.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-12-24 17:51:15
 4 |  * @Description: 超时中间件
 5 |  * @LastEditors: ShawnPhang <site: m.palxp.cn>
 6 |  * @LastEditTime: 2023-07-05 20:17:00
 7 |  */
 8 | 
 9 | export default async (req: any, res: any, next: any) => {
10 |   const { queueList } = require('../utils/node-queue.ts')
11 |   const time = 30000 // 设置所有HTTP请求的服务器响应超时时间
12 |   res.setTimeout(time, () => {
13 |     const statusCode = 408
14 |     const index = queueList.findIndex((x: any) => x.sign === req._queueSign)
15 |     if (index !== -1) {
16 |       queueList.splice(index, 1)
17 |       if (!res.headersSent) {
18 |         res.status(statusCode).json({
19 |           statusCode,
20 |           message: '响应超时,任务已取消,请重试',
21 |         })
22 |       }
23 |     }
24 |   })
25 |   next()
26 | }
27 | 


--------------------------------------------------------------------------------
/service/src/utils/tools.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-11 02:26:55
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 13:48:40
 7 |  */
 8 | import fs from 'fs'
 9 | import path from 'path'
10 | import { filesReader, copyFile, readFile } from './fs'
11 | 
12 | export const send = {
13 |   success: (res: any, result: any, msg: string = 'ok') => {
14 |     res.json({
15 |       code: 200,
16 |       msg,
17 |       result: result || undefined,
18 |     })
19 |   },
20 |   error: (res: any, msg: string = 'error') => {
21 |     res.json({
22 |       code: 400,
23 |       msg,
24 |     })
25 |   },
26 | }
27 | export const isNumber = (value: any) => {
28 |   return typeof value === 'number' && !isNaN(value)
29 | }
30 | 
31 | export const buildTree = (data: any[]) => {}
32 | 
33 | export const groupBy = (array: any[], property: any) => {}
34 | 
35 | // 检测目录并创建目录(支持深层级)
36 | export const checkCreateFolder = (folder: string) => {
37 |   try {
38 |     const pathArr = splitPath(folder)
39 |     let _path = ''
40 |     for (let i = 0; i < pathArr.length; i++) {
41 |       if (pathArr[i]) {
42 |         _path += `/${pathArr[i]}`
43 |         !fs.existsSync(_path) && fs.mkdirSync(_path)
44 |       }
45 |     }
46 |   } catch (e) {}
47 | }
48 | 
49 | // 检测文件
50 | export const checkCreateFile = (filePath: string) => {
51 |   try {
52 |     if (!fs.existsSync(filePath)) {
53 |       fs.writeFileSync(filePath, '')
54 |     }
55 |   } catch (e) {
56 |     fs.writeFileSync(filePath, '')
57 |   }
58 | }
59 | 
60 | // 生成随机码
61 | export const randomCode = (length = 5) => {
62 |   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
63 |   let result = ''
64 |   for (let i = 0; i < length; i++) {
65 |     const randomIndex = Math.floor(Math.random() * chars.length)
66 |     result += chars[randomIndex]
67 |   }
68 |   return result
69 | }
70 | 
71 | // 取数组差集
72 | export const findDifference = (a: any, b: any) => {
73 |   return a.concat(b).filter((v: any) => !a.includes(v) || !b.includes(v))
74 | }
75 | 
76 | export { copyFile, readFile, filesReader }
77 | 
78 | /**
79 |  * 将路径切割为数组
80 |  * @param dirPath
81 |  * @returns Array
82 |  */
83 | function splitPath(dirPath: string) {
84 |   const normalizedPath = path.normalize(dirPath)
85 |   const separator = path.sep
86 |   return normalizedPath.split(separator)
87 | }
88 | 
89 | 


--------------------------------------------------------------------------------
/service/src/utils/uuid.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: 侯超委 houchaowei@zhihu.com
 3 |  * @Date: 2023-09-01 14:33:23
 4 |  * @LastEditors: 侯超委 houchaowei@zhihu.com
 5 |  * @LastEditTime: 2023-09-01 14:56:38
 6 |  * @FilePath: /poster-design/screenshot/src/utils/uuid.ts
 7 |  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 8 |  */
 9 | 
10 | import nodeCrypto from 'crypto';
11 | 
12 | export default () =>
13 |   // @ts-ignore
14 |   ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c: number) =>
15 |     (c ^ (nodeCrypto.randomBytes(1)[0] & (15 >> (c / 4)))).toString(16)
16 |   );
17 | 


--------------------------------------------------------------------------------
/service/src/utils/widget/Device.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-06-09 17:54:26
 4 |  * @Description: 设备预设列表
 5 |  * @LastEditors: ShawnPhang <site: m.palxp.cn>
 6 |  * @LastEditTime: 2023-06-28 00:22:30
 7 |  */
 8 | const DevicesNames = [
 9 |     'Blackberry PlayBook',
10 |     'Blackberry PlayBook landscape',
11 |     'BlackBerry Z30',
12 |     'BlackBerry Z30 landscape',
13 |     'Galaxy Note 3',
14 |     'Galaxy Note 3 landscape',
15 |     'Galaxy Note II',
16 |     'Galaxy Note II landscape',
17 |     'Galaxy S III',
18 |     'Galaxy S III landscape',
19 |     'Galaxy S5',
20 |     'Galaxy S5 landscape',
21 |     'iPad',
22 |     'iPad landscape',
23 |     'iPad Mini',
24 |     'iPad Mini landscape',
25 |     'iPad Pro',
26 |     'iPad Pro landscape',
27 |     'iPhone 4',
28 |     'iPhone 4 landscape',
29 |     'iPhone 5',
30 |     'iPhone 5 landscape',
31 |     'iPhone 6',
32 |     'iPhone 6 landscape',
33 |     'iPhone 6 Plus',
34 |     'iPhone 6 Plus landscape',
35 |     'iPhone 7',
36 |     'iPhone 7 landscape',
37 |     'iPhone 7 Plus',
38 |     'iPhone 7 Plus landscape',
39 |     'iPhone 8',
40 |     'iPhone 8 landscape',
41 |     'iPhone 8 Plus',
42 |     'iPhone 8 Plus landscape',
43 |     'iPhone SE',
44 |     'iPhone SE landscape',
45 |     'iPhone X',
46 |     'iPhone X landscape',
47 |     'Kindle Fire HDX',
48 |     'Kindle Fire HDX landscape',
49 |     'LG Optimus L70',
50 |     'LG Optimus L70 landscape',
51 |     'Microsoft Lumia 550',
52 |     'Microsoft Lumia 950',
53 |     'Microsoft Lumia 950 landscape',
54 |     'Nexus 10',
55 |     'Nexus 10 landscape',
56 |     'Nexus 4',
57 |     'Nexus 4 landscape',
58 |     'Nexus 5',
59 |     'Nexus 5 landscape',
60 |     'Nexus 5X',
61 |     'Nexus 5X landscape',
62 |     'Nexus 6',
63 |     'Nexus 6 landscape',
64 |     'Nexus 6P',
65 |     'Nexus 6P landscape',
66 |     'Nexus 7',
67 |     'Nexus 7 landscape',
68 |     'Nokia Lumia 520',
69 |     'Nokia Lumia 520 landscape',
70 |     'Nokia N9',
71 |     'Nokia N9 landscape',
72 |     'Pixel 2',
73 |     'Pixel 2 landscape',
74 |     'Pixel 2 XL',
75 |     'Pixel 2 XL landscape'
76 |   ]


--------------------------------------------------------------------------------
/service/src/utils/widget/apidoc.js:
--------------------------------------------------------------------------------
 1 | /**
 2 | * @apiDefine needToken
 3 | * @apiHeader {String} Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
 4 | */
 5 | 
 6 | /**
 7 | * @apiDefine SuccessExampleName
 8 | * @apiSuccessExample {json} Success-Response:
 9 | *     HTTP/1.1 200 OK
10 | *     {
11 | *       "message": "ok"
12 | *     }
13 | */
14 | 


--------------------------------------------------------------------------------
/service/webpack.config.js:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-12-27 10:15:07
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2021-12-27 11:22:29
 7 |  */
 8 | 'use strict'
 9 | 
10 | const path = require('path')
11 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
12 | const nodeExternals = require('webpack-node-externals');
13 | const buildPlugin = require('./webpack.plugin.js');
14 | 
15 | module.exports = {
16 |   mode: process.env.NODE_ENV,
17 |   target: 'node',
18 |   externals: [nodeExternals()],
19 |   entry: './src/main.ts',
20 |   output: {
21 |     path: path.resolve(__dirname, 'dist'),
22 |     filename: 'server.js',
23 |   },
24 |   module: {
25 |     rules: [
26 |       {
27 |         test: /\.tsx?$/,
28 |         use: [
29 |           {
30 |             loader: 'ts-loader',
31 |             options: {
32 |               transpileOnly: true,
33 |               // 指定特定的ts编译配置,为了区分脚本的ts配置
34 |               configFile: path.resolve(__dirname, './tsconfig.json'),
35 |             },
36 |           },
37 |         ],
38 |         exclude: /node_modules/,
39 |       },
40 |     ],
41 |   },
42 |   resolve: {
43 |     extensions: ['.ts', '.tsx', '.js', '.json'],  // 解析的文件类型
44 |     alias: {
45 |       '@': path.resolve(__dirname, 'src')  // 配置路径别名,指向 src 目录
46 |     }
47 |   },
48 |   // plugins: [new BundleAnalyzerPlugin()],
49 |   plugins: [ new buildPlugin() ]
50 | }
51 | 


--------------------------------------------------------------------------------
/service/webpack.plugin.js:
--------------------------------------------------------------------------------
 1 | const pkg = require("./package.json");
 2 | 
 3 | class MyPlugin {
 4 | 	apply(compiler) {
 5 | 		compiler.hooks.emit.tapAsync("BuildPackageJson", (compilation, callback) => {
 6 | 			console.log("构建 package.json ....");
 7 | 
 8 | 			const myBuildPackageJson = {
 9 | 				name: `${pkg.name}-builder`,
10 | 				version: pkg.version,
11 | 				dependencies: pkg.dependencies
12 | 			};
13 | 
14 | 			compilation.assets['package.json'] = {
15 | 				source: () => JSON.stringify(myBuildPackageJson, null, 2),
16 | 				size: () => Buffer.byteLength(JSON.stringify(myBuildPackageJson, null, 2), 'utf8')
17 | 			};
18 | 
19 | 			console.log('package.json 文件构建完成!');
20 | 			callback();
21 | 		});
22 | 	}
23 | }
24 | 
25 | module.exports = MyPlugin;
26 | 


--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div id="app-view">
 3 |     <el-config-provider :locale="locale">
 4 |       <router-view />
 5 |     </el-config-provider>
 6 |   </div>
 7 | </template>
 8 | 
 9 | <script setup lang="ts">
10 | import { computed, onMounted } from 'vue'
11 | import { ElConfigProvider } from 'element-plus'
12 | import en from 'element-plus/es/locale/lang/en'
13 | import zhCn from 'element-plus/es/locale/lang/zh-cn'
14 | import { useI18n } from 'vue-i18n'
15 | import { getBrowserLang } from './languages'
16 | 
17 | const lang = getBrowserLang()
18 | 
19 | const i18n = useI18n()
20 | onMounted(() => {
21 |   i18n.locale.value = lang
22 | })
23 | 
24 | // 配置语言,否则element默认是英语
25 | const locale = computed(() => {
26 |   return lang == 'zh' ? zhCn : en
27 | })
28 | </script>
29 | 
30 | <style lang="less">
31 | #app-view {
32 |   min-width: 1180px;
33 | }
34 | </style>
35 | 


--------------------------------------------------------------------------------
/src/api/ai.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-27 14:42:15
 4 |  * @Description: AI相关接口
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @Date: 2024-03-03 19:00:00
 7 |  */
 8 | import fetch from '@/utils/axios'
 9 | 
10 | export type TCommonUploadCb = (up: number, dp: number) => void
11 | 
12 | type TUploadProgressCbData = {
13 |   loaded: number
14 |   total: number
15 | }
16 | 
17 | export type TUploadErrorResult = {type: "application/json"}
18 | 
19 | // 上传接口
20 | export const upload = (file: File, cb: TCommonUploadCb) => {
21 |   const formData = new FormData()
22 |   formData.append('file', file)
23 |   const extra = {
24 |     responseType: 'blob',
25 |     onUploadProgress: (progress: TUploadProgressCbData) => {
26 |       cb(Math.floor((progress.loaded / progress.total) * 100), 0)
27 |     },
28 |     onDownloadProgress: (progress: TUploadProgressCbData) => {
29 |       cb(100, Math.floor((progress.loaded / progress.total) * 100))
30 |     },
31 |   }
32 |   return fetch<MediaSource | TUploadErrorResult>('https://res.palxp.cn/ai/upload', formData, 'post', {}, extra)
33 | }
34 | 


--------------------------------------------------------------------------------
/src/api/album.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-26 12:47:40
 4 |  * @Description: 相册 api 接口
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2021-08-30 10:45:49
 7 |  */
 8 | import fetch from '@/utils/axios'
 9 | import _config from '@/config'
10 | const prefix = _config.API_URL + '/'
11 | const API = {
12 |   init: prefix + 'pic/init',
13 |   getList: prefix + 'pic/list',
14 |   getToken: prefix + 'pic/getToken',
15 |   delOne: prefix + 'pic/delOne',
16 |   rename: prefix + 'pic/rename',
17 |   del: prefix + 'pic/del',
18 | }
19 | 
20 | export const init = (params: Type.Object = {}) => fetch(API.init, params, 'post')
21 | 
22 | export const getPicList = (params: Type.Object = {}) => fetch(API.getList, params)
23 | 
24 | type TGetTokenParam = {
25 |   bucket: string,
26 |   name: string
27 | }
28 | 
29 | export const getToken = (params: TGetTokenParam) => fetch<string>(API.getToken, params)
30 | 
31 | export const deletePic = (params: Type.Object = {}) => fetch(API.delOne, params, 'post')
32 | 
33 | export const delPics = (params: Type.Object = {}) => fetch(API.del, params, 'post')
34 | 
35 | export const reName = (params: Type.Object = {}) => fetch(API.rename, params, 'post')
36 | 
37 | export default {
38 |   init,
39 |   getPicList,
40 |   getToken,
41 |   deletePic,
42 |   delPics,
43 |   reName,
44 | }
45 | 


--------------------------------------------------------------------------------
/src/api/github.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-07-13 17:01:37
 4 |  * @Description: github api
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-08-10 10:33:59
 7 |  */
 8 | import fetch from '@/utils/axios'
 9 | const cutToken = 'ghp_qpV8PUxwY7as4jc'
10 | 
11 | const reader = new FileReader()
12 | function getBase64(file: File) {
13 |   return new Promise((resolve) => {
14 |     reader.onload = function (event) {
15 |       const fileContent = event.target && event.target.result
16 |       resolve((fileContent as string).split(',')[1])
17 |     }
18 |     reader.readAsDataURL(file)
19 |   })
20 | }
21 | 
22 | const putPic = async (file: File | string) => {
23 |   const repo = 'shawnphang/files'
24 |   const d = new Date()
25 |   const content = typeof file === 'string' ? file : await getBase64(file)
26 |   const extra = typeof file === 'string' ? '.png' : file.name?.split('.').pop()
27 |   const path = `${d.getFullYear()}/${d.getMonth()}/${d.getTime()}${extra}`
28 |   const imageUrl = 'https://api.github.com/repos/' + repo + '/contents/' + path
29 |   const body = { branch: 'main', message: 'upload', content, path }
30 |   const res = await fetch(imageUrl, body, 'put', {
31 |     Authorization: `token ${cutToken}AqYfNFb6G2f2OVl4IVFOY`,
32 |     'Content-Type': 'application/json; charset=utf-8',
33 |   })
34 |   return res?.content?.download_url || `https://fastly.jsdelivr.net/gh/shawnphang/files@main/${path}`
35 | }
36 | 
37 | export default { putPic }
38 | 


--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-19 18:43:22
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-09-28 15:46:56
 7 |  */
 8 | import * as home from './home'
 9 | import * as material from './material'
10 | import * as ai from './ai'
11 | 
12 | export default {
13 |   home,
14 |   material,
15 |   ai,
16 | }
17 | 


--------------------------------------------------------------------------------
/src/assets/data/AlignListData.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-12 11:08:57
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-01 20:55:51
 7 |  */
 8 | 
 9 | export type AlignListData = {
10 |   key: string
11 |   icon: string
12 |   tip: string
13 |   value: string
14 | }
15 | 
16 | export default [
17 |   {
18 |     key: 'align',
19 |     icon: 'icon-align-left',
20 |     tip: '左对齐',
21 |     value: 'left',
22 |   },
23 |   {
24 |     key: 'align',
25 |     icon: 'icon-align-center-horiz',
26 |     tip: '水平居中对齐',
27 |     value: 'ch',
28 |   },
29 |   {
30 |     key: 'align',
31 |     icon: 'icon-align-right',
32 |     tip: '右对齐',
33 |     value: 'right',
34 |   },
35 |   {
36 |     key: 'align',
37 |     icon: 'icon-align-top',
38 |     tip: '上对齐',
39 |     value: 'top',
40 |   },
41 |   {
42 |     key: 'align',
43 |     icon: 'icon-align-center-verti',
44 |     tip: '垂直居中对齐',
45 |     value: 'cv',
46 |   },
47 |   {
48 |     key: 'align',
49 |     icon: 'icon-align-bottom',
50 |     tip: '下对齐',
51 |     value: 'bottom',
52 |   },
53 | ] as AlignListData[]
54 | 


--------------------------------------------------------------------------------
/src/assets/data/LayerIconList.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-04-15 10:51:50
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-01 20:55:51
 7 |  */
 8 | 
 9 | import { TIconItemSelectData } from "@/components/modules/settings/iconItemSelect.vue"
10 | 
11 | export default [
12 |   {
13 |     key: 'zIndex',
14 |     icon: 'icon-layer-up',
15 |     tip: '上一层',
16 |     value: 1,
17 |   },
18 |   {
19 |     key: 'zIndex',
20 |     icon: 'icon-layer-down',
21 |     tip: '下一层',
22 |     value: -1,
23 |   },
24 | ] as TIconItemSelectData[]
25 | 


--------------------------------------------------------------------------------
/src/assets/data/PageSizeData.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-07 17:49:06
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-10 00:37:33
 7 |  */
 8 | export default [
 9 |   {
10 |     name: '手机海报',
11 |     width: 1242,
12 |     height: 2208,
13 |     icon: 'sd-shouji'
14 |   },
15 |   {
16 |     name: '横版海报',
17 |     width: 900,
18 |     height: 500,
19 |     icon: 'sd-wangye'
20 |   },
21 |   {
22 |     name: '公众号首图',
23 |     width: 900,
24 |     height: 383,
25 |     icon: 'sd-weixin'
26 |   },
27 |   {
28 |     name: '公众号次图',
29 |     width: 500,
30 |     height: 500,
31 |     icon: 'sd-weixin'
32 |   },
33 |   {
34 |     name: '小红书配图',
35 |     width: 1242,
36 |     height: 1660,
37 |     icon: 'sd-shouji'
38 |   },
39 |   {
40 |     name: '商品主图',
41 |     width: 800,
42 |     height: 800,
43 |     icon: 'sd-wangye'
44 |   },
45 |   {
46 |     name: '电商详情页',
47 |     width: 750,
48 |     height: 1000,
49 |     icon: 'sd-wangye'
50 |   },
51 |   {
52 |     name: '电商竖版海报',
53 |     width: 750,
54 |     height: 950,
55 |     icon: 'sd-shouji'
56 |   },
57 |   {
58 |     name: '电商横版海报',
59 |     width: 750,
60 |     height: 390,
61 |     icon: 'sd-wangye'
62 |   },
63 |   {
64 |     name: '小程序封面',
65 |     width: 520,
66 |     height: 416,
67 |     icon: 'sd-weixin'
68 |   },
69 |   {
70 |     name: '壁纸 / PPT(16:9)',
71 |     width: 1920,
72 |     height: 1080,
73 |     icon: 'sd-wangye'
74 |   }
75 | ]
76 | 


--------------------------------------------------------------------------------
/src/assets/data/QrCodeLocalization.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-16 11:38:48
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-01 20:55:51
 7 |  */
 8 | 
 9 | export type QrCodeLocalizationData = {
10 |   dotColorTypes: {
11 |     key: string
12 |     value: string
13 |   }[]
14 |   dotTypes: {
15 |     key: string
16 |     value: string
17 |   }[]
18 | }
19 | 
20 | export default {
21 |   dotColorTypes: [
22 |     {
23 |       key: 'single',
24 |       value: '单色',
25 |     },
26 |     {
27 |       key: 'gradient',
28 |       value: '渐变色',
29 |     },
30 |   ],
31 |   dotTypes: [
32 |     {
33 |       key: 'dots',
34 |       value: '圆点风格',
35 |     },
36 |     {
37 |       key: 'rounded',
38 |       value: '圆润风格',
39 |     },
40 |     {
41 |       key: 'classy',
42 |       value: '经典风格',
43 |     },
44 |     {
45 |       key: 'classy-rounded',
46 |       value: '圆角风格',
47 |     },
48 |     {
49 |       key: 'square',
50 |       value: '方形风格',
51 |     },
52 |     {
53 |       key: 'extra-rounded',
54 |       value: '特殊风格',
55 |     },
56 |   ],
57 | } as QrCodeLocalizationData
58 | 


--------------------------------------------------------------------------------
/src/assets/data/WidgetClassifyList.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-07-17 11:20:22
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-01 20:55:51
 7 |  */
 8 | 
 9 | import { StyleValue } from "vue"
10 | 
11 | export type TWidgetClassifyData = {
12 |   name: string
13 |   icon: string
14 |   show: boolean
15 |   component: string
16 |   style?: StyleValue
17 | }
18 | 
19 | export default [
20 |   {
21 |     name: '模板',
22 |     icon: 'icon-moban',
23 |     show: false,
24 |     component: 'temp-list-wrap',
25 |   },
26 |   {
27 |     name: '素材',
28 |     icon: 'icon-sucai',
29 |     show: false,
30 |     component: 'graph-list-wrap',
31 |   },
32 |   {
33 |     name: '文字',
34 |     icon: 'icon-wenzi',
35 |     show: false,
36 |     style: { fontWeight: 600 },
37 |     component: 'text-list-wrap',
38 |   },
39 |   {
40 |     name: '照片',
41 |     icon: 'icon-gallery',
42 |     show: false,
43 |     component: 'photo-list-wrap',
44 |   },
45 |   // {
46 |   //   name: '背景',
47 |   //   icon: 'icon-beijing',
48 |   //   show: false,
49 |   //   component: 'bg-img-list-wrap',
50 |   // },
51 |   {
52 |     name: '工具箱',
53 |     icon: 'icon-zujian01',
54 |     show: false,
55 |     component: 'tools-list-wrap',
56 |   },
57 |   {
58 |     name: '我的',
59 |     icon: 'icon-shangchuan',
60 |     show: false,
61 |     component: 'user-wrap',
62 |   },
63 | ] as TWidgetClassifyData[]
64 | 


--------------------------------------------------------------------------------
/src/assets/fonts/xpsj.subset.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palxiao/poster-design/68d3e7bffa36950f3740ed92744766102a910bae/src/assets/fonts/xpsj.subset.ttf


--------------------------------------------------------------------------------
/src/assets/fonts/xpsj.subset.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palxiao/poster-design/68d3e7bffa36950f3740ed92744766102a910bae/src/assets/fonts/xpsj.subset.woff2


--------------------------------------------------------------------------------
/src/assets/styles/color.less:
--------------------------------------------------------------------------------
 1 | // @color-main: #1b1634;
 2 | @color-main: #ffffff;
 3 | @color-white: #ffffff;
 4 | @color-black: #000000;
 5 | @color-light-gray: #3e4651;
 6 | @color-dark-gray: #262c33;
 7 | @color-transparent: #ffffff00;
 8 | 
 9 | // @active-text-color: rgb(250, 131, 52);
10 | // @main-color: rgb(250, 131, 52);
11 | @active-text-color: #2254f4; // #1195db;
12 | @main-color: #2254f4; // #1195db;
13 | 


--------------------------------------------------------------------------------
/src/assets/styles/index.less:
--------------------------------------------------------------------------------
1 | @import './main.less';
2 | @import './layout.less';
3 | @import './base.less';
4 | 


--------------------------------------------------------------------------------
/src/assets/styles/layout.less:
--------------------------------------------------------------------------------
 1 | .editable-text {
 2 |   outline: none;
 3 |   word-break: break-word;
 4 |   white-space: pre;
 5 |   margin: 0;
 6 | }
 7 | 
 8 | .line-clamp-2 {
 9 |   // display: -webkit-box !important;
10 |   // -webkit-box-orient: vertical !important;
11 |   // overflow: hidden !important;
12 |   overflow: hidden;
13 |   text-overflow: ellipsis;
14 |   display: -webkit-box; //作为弹性伸缩盒子模型显示。
15 |   -webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
16 | }
17 | 
18 | .line-clamp-1 {
19 |   // -webkit-line-clamp: 1;
20 |   overflow: hidden;
21 |   text-overflow: ellipsis;
22 |   white-space: nowrap;
23 | }
24 | 
25 | .line-clamp-2 {
26 |   -webkit-line-clamp: 2;
27 | }
28 | 
29 | .transparent-bg {
30 |   background-color: #f0f0f0;
31 |   background-image: linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), linear-gradient(to top right, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff);
32 |   background-position: 0 0, 8px 8px;
33 |   background-size: 16px 16px;
34 |   overflow: hidden;
35 |   user-select: none;
36 | }
37 | 


--------------------------------------------------------------------------------
/src/common/methods/DesignFeatures/setComponents.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-22 15:06:14
 4 |  * @Description: 设置图片类型元素
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-02 11:50:00
 7 |  */
 8 | import { useCanvasStore } from '@/store'
 9 | import { TdWidgetData } from '@/store/design/widget'
10 | // import store from '@/store'
11 | 
12 | export default async function setCompData(item: TdWidgetData[] | string) {
13 |   const canvasStore = useCanvasStore()
14 |   const group: TdWidgetData[] = typeof item === 'string' ? JSON.parse(item) : JSON.parse(JSON.stringify(item))
15 |   let parent: Partial<TdWidgetData> = {}
16 |   Array.isArray(group) &&
17 |     group.forEach((element) => {
18 |       element.type === 'w-group' && (parent = element)
19 |     })
20 |   const { width: screenWidth, height: screenHeight } = canvasStore.dPage
21 |   const { width: imgWidth = 0, height: imgHeight = 0 } = parent
22 |   let ratio = 1
23 |   // 先限制在画布内,保证不超过边界
24 |   if (imgWidth > screenWidth || imgHeight > screenHeight) {
25 |     ratio = Math.min(screenWidth / imgWidth, screenHeight / imgHeight)
26 |   }
27 |   // 根据画布缩放比例再进行一次调整
28 |   if (ratio < 1) {
29 |     ratio *= canvasStore.dZoom / 100
30 |     group.forEach((element) => {
31 |       element.fontSize && (element.fontSize *= ratio)
32 |       element.width *= ratio
33 |       element.height *= ratio
34 |       element.left *= ratio
35 |       element.top *= ratio
36 |     })
37 |   }
38 |   return group
39 | }
40 | 


--------------------------------------------------------------------------------
/src/common/methods/DesignFeatures/setImage.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-22 15:06:14
 4 |  * @Description: 设置图片类型元素
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-01 20:55:51
 7 |  */
 8 | // import store from '@/store'
 9 | import { getImage } from '../getImgDetail'
10 | import { useCanvasStore } from '@/store'
11 | 
12 | export type TItem2DataParam = {
13 |   id?: string | number
14 |   width: number
15 |   height: number
16 |   url: string
17 |   model?: string
18 |   canvasWidth?: number
19 | }
20 | 
21 | export type TItem2DataResult = {
22 |   width: number
23 |   height: number
24 |   canvasWidth: number
25 | }
26 | 
27 | export default async function setItem2Data(item: TItem2DataParam): Promise<Required<TItem2DataParam>> {
28 |   const canvasStore = useCanvasStore()
29 |   const cloneItem = JSON.parse(JSON.stringify(item))
30 |   const { width: screenWidth, height: screenHeight } = canvasStore.dPage
31 |   let { width: imgWidth, height: imgHeight } = item
32 |   if (!imgWidth || !imgHeight) {
33 |     const actual = await getImage(item.url)
34 |     cloneItem.width = imgWidth = actual.width
35 |     cloneItem.height = imgHeight = actual.height
36 |   }
37 |   let ratio = 1
38 |   // 先限制在画布内,保证不超过边界
39 |   if (imgWidth > screenWidth || imgHeight > screenHeight) {
40 |     ratio = Math.min(screenWidth / imgWidth, screenHeight / imgHeight)
41 |   }
42 |   // 根据画布缩放比例再进行一次调整
43 |   if (ratio < 1) {
44 |     cloneItem.width = cloneItem.width * ratio * (canvasStore.dZoom / 100)
45 |     cloneItem.height = cloneItem.height * ratio * (canvasStore.dZoom / 100)
46 |   }
47 | 
48 |   cloneItem.canvasWidth = cloneItem.width * (canvasStore.dZoom / 100)
49 |   // cloneItem.canvasHeight = cloneItem.height * (store.getters.dZoom / 100)
50 |   return cloneItem
51 | }
52 | 


--------------------------------------------------------------------------------
/src/common/methods/DesignFeatures/setWidgetData.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-22 15:06:14
 4 |  * @Description: 设置元素时根据类型处理
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-03-22 16:00:17
 7 |  */
 8 | // import store from '@/store'
 9 | // import { getImage } from '../getImgDetail'
10 | import setImageData from '@/common/methods/DesignFeatures/setImage'
11 | // import wText from '@/components/modules/widgets/wText/wText.vue'
12 | import { wTextSetting } from '@/components/modules/widgets/wText/wTextSetting'
13 | // import wImage from '@/components/modules/widgets/wImage/wImage.vue'
14 | import wImageSetting from '@/components/modules/widgets/wImage/wImageSetting'
15 | import { wSvgSetting } from '@/components/modules/widgets/wSvg/wSvgSetting'
16 | 
17 | export default async function(type: string, item: TCommonItemData, data: Record<string, any>) {
18 |   let setting = data
19 |   if (type === 'text') {
20 |     !item.fontFamily && !item.color ? (setting = JSON.parse(JSON.stringify(wTextSetting))) : (setting = item)
21 |     !setting.text ? (setting.text = '双击编辑文字') : (setting.text = decodeURIComponent(setting.text)) // item.text
22 |     setting.fontSize = item.fontSize
23 |     setting.width = item.width || item.fontSize * setting.text.length
24 |     setting.fontWeight = item.fontWeight
25 |   }
26 |   if (type === 'image' || type === 'mask') {
27 |     setting = JSON.parse(JSON.stringify(wImageSetting))
28 |     const img = await setImageData(item.value)
29 |     setting.width = img.width
30 |     setting.height = img.height // parseInt(100 / item.value.ratio, 10)
31 |     setting.imgUrl = item.value.url
32 |   }
33 |   if (type === 'mask') {
34 |     setting.mask = item.value.url
35 |   }
36 |   if (type === 'svg') {
37 |     setting = JSON.parse(JSON.stringify(wSvgSetting))
38 |     const img = await setImageData(item.value)
39 |     setting.width = img.width
40 |     setting.height = img.height // parseInt(100 / item.value.ratio, 10)
41 |     setting.svgUrl = item.value.url
42 |     const models = JSON.parse(item.value.model)
43 |     for (const key in models) {
44 |       if (Object.hasOwnProperty.call(models, key)) {
45 |         setting[key] = models[key]
46 |       }
47 |     }
48 |   }
49 |   return setting
50 | }
51 | 


--------------------------------------------------------------------------------
/src/common/methods/QiNiu.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-29 20:35:31
 4 |  * @Description: 七牛上传方法
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-02 11:50:00
 7 |  */
 8 | import dayjs from 'dayjs'
 9 | import api from '@/api/album'
10 | 
11 | interface Options {
12 |   bucket: string
13 |   prePath?: string
14 |   fullPath?: string
15 | }
16 | 
17 | export default {
18 |   upload: async (file: File | Blob, options: Options, cb?: IQiniuSubscribeCb) => {
19 |     const win = window
20 |     let name = ''
21 |     const suffix = file.type.split('/')[1] || 'png' // 文件后缀
22 |     if (!options.fullPath) {
23 |       // const DT: any = await exifGetTime(file) // 照片时间
24 |       const DT = new Date()
25 |       const YM = `${dayjs(DT).format('YYYY')}/${dayjs(DT).format('MM')}/` // 文件时间分类
26 |       const keyName = YM + new Date(DT).getTime()
27 |       const prePath = options.prePath ? options.prePath + '/' : ''
28 |       name = `${prePath}${keyName}` + `.${suffix}` // 文件名
29 |     } else name = options.fullPath + `.${suffix}` // 文件名
30 |     const token = await api.getToken({ bucket: options.bucket, name })
31 |     const exOption = {
32 |       useCdnDomain: true, // 使用cdn加速
33 |     }
34 |     const observable = win.qiniu.upload(file, name, token, {}, exOption)
35 |     return new Promise((resolve: IQiniuSubscribeCb, reject: (err: string) => void) => {
36 |       observable.subscribe({
37 |         next: (result) => {
38 |           cb?.(result) // result.total.percent -> 展示进度
39 |         },
40 |         error: (e) => {
41 |           reject(e)
42 |         },
43 |         complete: (result) => {
44 |           resolve(result)
45 |           // cb && cb(result) // result.total.percent -> 展示进度
46 |         },
47 |       })
48 |     })
49 |   },
50 | }
51 | 
52 | // function exifGetTime(img: any) {
53 | //   const win = window as any
54 | //   return new Promise((resolve) => {
55 | //     const file = img.originFileObj || img
56 | //     win.EXIF.getData(file, function() {
57 | //       const DT = win.EXIF.getAllTags(this).DateTimeOriginal || win.EXIF.getAllTags(this).DateTime
58 | //       if (DT) {
59 | //         if (DT.split(' ').length > 1) {
60 | //           const date = DT.split(' ')[0].replace(/:/g, '/')
61 | //           const time = DT.split(' ')[1]
62 | //           resolve(dayjs(`${date} ${time}`).isValid() ? `${date} ${time}` : date)
63 | //         } else {
64 | //           resolve(DT)
65 | //         }
66 | //       } else {
67 | //         resolve(new Date())
68 | //       }
69 | //     })
70 | //   })
71 | // }
72 | 


--------------------------------------------------------------------------------
/src/common/methods/addMouseWheel.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-25 13:43:07
 4 |  * @Description: 添加滚动监听
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-02 11:50:00
 7 |  */
 8 | import { useControlStore } from '@/store'
 9 | // import store from '@/store'
10 | 
11 | type TAddEventCb = (e: Event) => void
12 | type TAddEventObj = {
13 |   attachEvent?: HTMLElement["addEventListener"]
14 | } & HTMLElement
15 | 
16 | export default function(el: HTMLElement | string, cb: Function, altLimit: boolean = true) {
17 |   const box = typeof el === 'string' ? document.getElementById(el) : el
18 |   const controlStore = useControlStore()
19 |   if (!box) return;
20 |   addEvent(box, 'mousewheel', (e: any) => {
21 |     const ev = e || window.event
22 |     const down = ev.wheelDelta ? ev.wheelDelta < 0 : ev.detail > 0
23 |     // if (down) {
24 |     //   console.log('鼠标滚轮向下---------')
25 |     // } else {
26 |     //   console.log('鼠标滚轮向上++++++++++')
27 |     // }
28 |     if ((altLimit && controlStore.dAltDown) || !altLimit) {
29 |       ev.preventDefault()
30 |       cb(down)
31 |     }
32 |     return false
33 |   })
34 | }
35 | 
36 | function addEvent(obj: TAddEventObj, xEvent: keyof HTMLElementEventMap, fn: TAddEventCb) {
37 |   if (obj.attachEvent) {
38 |     obj.attachEvent('on' + xEvent, fn)
39 |   } else {
40 |     obj.addEventListener(xEvent, fn, false)
41 |   }
42 | }
43 | 


--------------------------------------------------------------------------------
/src/common/methods/confirm.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-02-03 16:30:18
 4 |  * @Description: Type: success / info / warning / error
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-08 18:55:12
 7 |  */
 8 | import { ElMessageBox, messageType } from 'element-plus'
 9 | export default (title: string = '提示', message: string = '', type: messageType = 'success', extra?: any) => {
10 |   return new Promise((resolve: Function) => {
11 |     ElMessageBox.confirm(message, title, Object.assign({
12 |       confirmButtonText: '确定',
13 |       cancelButtonText: '取消',
14 |       type,
15 |     }, extra))
16 |       .then(() => {
17 |         resolve(true)
18 |       })
19 |       .catch(() => {
20 |         resolve(false)
21 |       })
22 |   })
23 | }
24 | 


--------------------------------------------------------------------------------
/src/common/methods/download/download.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-09-30 15:52:59
 4 |  * @Description: 下载远程图片
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 17:01:59
 7 |  */
 8 | 
 9 | type TCallBack = (progress: number, xhr: XMLHttpRequest) => void
10 | 
11 | export default (src: string, cb: TCallBack, fileName?: string) => {
12 |   return new Promise<void>((resolve) => {
13 |     fetchImageDataFromUrl(src, (progress: number, xhr: XMLHttpRequest) => {
14 |       cb(progress, xhr)
15 |     }).then((res: any) => {
16 |       const reader = new FileReader()
17 |       reader.onload = function (event) {
18 |         const a = document.createElement('a')
19 |         const mE = new MouseEvent('click')
20 |         const suffix = res.type ? res.type.split('/')[1] : 'png'
21 |         const randomName = String(new Date().getTime()) + `.${suffix || 'png'}`
22 |         a.download = fileName || randomName
23 |         a.href = event?.target?.result as string
24 |         // 触发a的单击事件
25 |         a.dispatchEvent(mE)
26 |         resolve(res)
27 |       }
28 |       if (!res) {
29 |         resolve()
30 |         return
31 |       }
32 |       reader.readAsDataURL(res)
33 |     })
34 |   })
35 | }
36 | 
37 | function fetchImageDataFromUrl(url: string, cb: TCallBack) {
38 |   return new Promise<null>((resolve) => {
39 |     const xhr = new XMLHttpRequest()
40 |     let totalLength: string | number = ''
41 |     xhr.open('GET', url)
42 |     xhr.responseType = 'blob'
43 |     xhr.onreadystatechange = function () {
44 |       totalLength = Number(xhr.getResponseHeader('content-length')) // 'cache-control'
45 |     }
46 |     xhr.onprogress = function (event) {
47 |       cb((event.loaded / Number(totalLength)) * 100, xhr)
48 |     }
49 |     xhr.onload = function () {
50 |       if (xhr.status < 400) resolve(this.response)
51 |       else resolve(null)
52 |     }
53 |     xhr.onerror = function (e) {
54 |       resolve(null)
55 |     }
56 |     xhr.send()
57 |   })
58 | }
59 | 


--------------------------------------------------------------------------------
/src/common/methods/download/downloadBase64File.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-07-12 19:37:14
 4 |  * @Description: 下载 base64
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-07-12 19:37:39
 7 |  */
 8 | export default (base64Data: string, fileName: string) => {
 9 |   const link = document.createElement('a')
10 |   link.href = base64Data
11 |   link.download = fileName
12 |   link.click()
13 | }
14 | 


--------------------------------------------------------------------------------
/src/common/methods/download/downloadBlob.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-07-12 19:36:16
 4 |  * @Description: 下载 blob
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-07-12 19:36:41
 7 |  */
 8 | export default (blob: Blob, fileName: string) => {
 9 |   const link = document.createElement('a')
10 |   link.href = URL.createObjectURL(blob)
11 |   link.download = fileName
12 |   document.body.appendChild(link)
13 |   link.click()
14 |   document.body.removeChild(link)
15 | }
16 | 


--------------------------------------------------------------------------------
/src/common/methods/download/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-07-12 16:54:12
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-07-12 19:38:09
 7 |  */
 8 | import downloadImg from './download'
 9 | import downloadBase64File from './downloadBase64File'
10 | 
11 | export default {
12 |   downloadImg,
13 |   downloadBase64File,
14 | }
15 | 


--------------------------------------------------------------------------------
/src/common/methods/getImgDetail.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-23 17:25:35
 4 |  * @Description: 获取图片细节的相关方法
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-09 10:42:54
 7 |  */
 8 | export const getImage = (imgItem: string | File): Promise<HTMLImageElement> => {
 9 |   // 创建对象
10 |   const img = new Image()
11 | 
12 |   // 改变图片的src
13 |   const url = window.URL || window.webkitURL
14 |   img.src = typeof imgItem === 'string' ? imgItem : url.createObjectURL(imgItem)
15 | 
16 |   return new Promise((resolve) => {
17 |     // 判断是否有缓存
18 |     if (img.complete) {
19 |       resolve(img)
20 |     } else {
21 |       // 加载完成执行
22 |       img.onload = function () {
23 |         resolve(img)
24 |       }
25 |     }
26 |   })
27 | }
28 | 


--------------------------------------------------------------------------------
/src/common/methods/handleTransform.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-01-31 10:45:53
 4 |  * @Description: 用于修改transform字符串
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-02 11:50:00
 7 |  */
 8 | export function getTransformAttribute(target: HTMLElement, attr: string = '') {
 9 |   const tf = target.style.transform
10 |   const iof = tf.indexOf(attr)
11 |   const half = tf.substring(iof + attr.length + 1)
12 |   return half.substring(0, half.indexOf(')'))
13 | }
14 | 
15 | export function setTransformAttribute(target: HTMLElement, attr: string, value: string | number = 0) {
16 |   const tf = target?.style.transform
17 |   if (!tf) {
18 |     return
19 |   }
20 |   const iof = tf.indexOf(attr)
21 |   const FRONT = tf.slice(0, iof + attr.length + 1)
22 |   const half = tf.substring(iof + attr.length + 1)
23 |   const END = half.substring(half.indexOf(')'))
24 |   target.style.transform = FRONT + value + END
25 | }
26 | 
27 | export function getMatrix(params: Record<string, any>) {
28 |   const result = []
29 |   for (const key in params) {
30 |     if (Object.hasOwn(params, key)) {
31 |       result.push(params[key])
32 |     }
33 |   }
34 |   return result
35 | }
36 | 


--------------------------------------------------------------------------------
/src/common/methods/helper/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-04-15 11:16:20
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-04-15 11:22:49
 7 |  */
 8 | /**
 9 |  * 显示全局提示
10 |  * @param content
11 |  * @param tooltipVisible
12 |  * @returns
13 |  */
14 | export function toolTip(content: string) {
15 |   const tooltip = drawTooltip(content)
16 |   document.body.appendChild(tooltip)
17 |   setTimeout(() => tooltip?.parentNode?.removeChild(tooltip), 2000)
18 | }
19 | 
20 | function drawTooltip(content: string, tooltipVisible = true) {
21 |   const tooltip: any = document.createElement('div')
22 |   tooltip.id = 'color-pipette-tooltip-container'
23 |   tooltip.innerHTML = content
24 |   tooltip.style = `
25 |     position: fixed;
26 |     left: 50%;
27 |     top: 9%;
28 |     z-index: 10002;
29 |     display: ${tooltipVisible ? 'flex' : 'none'};
30 |     align-items: center;
31 |     background-color: rgba(0,0,0,0.4);
32 |     padding: 6px 12px;
33 |     border-radius: 4px;
34 |     color: #fff;
35 |     font-size: 18px;
36 |     pointer-events: none;
37 |   `
38 |   return tooltip
39 | }
40 | 


--------------------------------------------------------------------------------
/src/common/methods/loading.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-09-30 16:28:40
 4 |  * @Description: 加载遮罩 / 弹窗
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-01-20 17:46:20
 7 |  */
 8 | import { ElLoading } from 'element-plus'
 9 | export default (text: string = 'loading') => {
10 |   const loading = ElLoading.service({
11 |     lock: true,
12 |     text,
13 |     spinner: 'el-icon-loading',
14 |     background: 'rgba(0, 0, 0, 0.7)',
15 |   })
16 |   return loading
17 |   // loading.close()
18 | }
19 | 


--------------------------------------------------------------------------------
/src/common/methods/notification.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-09-30 16:28:40
 4 |  * @Description: 弹出提示
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-01-20 18:19:20
 7 |  */
 8 | import { ElNotification } from 'element-plus'
 9 | 
10 | interface ElNotifi {
11 |   type?: 'success' | 'warning' | 'info' | 'error' | ''
12 |   position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
13 | }
14 | 
15 | export default (title: string, message: string = '', extra?: ElNotifi) => {
16 |   ElNotification({
17 |     title,
18 |     message,
19 |     ...extra,
20 |   })
21 | }
22 | 


--------------------------------------------------------------------------------
/src/common/methods/target.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-10 15:42:12
 4 |  * @Description: 处理与目标组件相关
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-03-13 16:17:54
 7 |  */
 8 | // TODO: Group类型比较特殊,所以需要全量循环并判断是否为group
 9 | const arr = ['w-text', 'w-image', 'w-svg', 'w-group', 'w-qrcode']
10 | 
11 | export function getTarget(currentTarget: HTMLElement): Promise<HTMLElement | null> {
12 |   let collector: string[] = []
13 |   let groupTarger: HTMLElement | null = null
14 |   let saveTarger: HTMLElement | null = null
15 |   return new Promise((resolve) => {
16 |     function findTarget(target: HTMLElement | null) {
17 |       if (!target || target.id === 'page-design') {
18 |         if (collector.length > 1) {
19 |           resolve(groupTarger)
20 |         } else {
21 |           resolve(saveTarger ?? currentTarget)
22 |         }
23 |         return
24 |       }
25 |       const t = Array.from(target.classList)
26 | 
27 |       collector = collector.concat(
28 |         t.filter((x) => {
29 |           arr.includes(x) && (saveTarger = target)
30 |           x === 'w-group' && (groupTarger = target)
31 |           return arr.includes(x)
32 |         }),
33 |       )
34 |       findTarget(target.parentElement)
35 |     }
36 |     findTarget(currentTarget)
37 |   })
38 | }
39 | 
40 | export function getFinalTarget(currentTarget: HTMLElement) {
41 |   let collector: string[] = []
42 |   // let groupTarger: HTMLElement | null = null
43 |   // let saveTarger: HTMLElement | null = null
44 |   return new Promise((resolve) => {
45 |     function findTarget(target: HTMLElement | null) {
46 |       if (!target || target.id === 'page-design') {
47 |         resolve(target)
48 |         return
49 |       }
50 |       const t = Array.from(target.classList)
51 | 
52 |       collector = collector.concat(
53 |         t.filter((x) => {
54 |           // arr.includes(x) && (saveTarger = target)
55 |           // x === 'w-group' && (groupTarger = target)
56 |           return arr.includes(x)
57 |         }),
58 |       )
59 | 
60 |       findTarget(target.parentElement)
61 |     }
62 |     findTarget(currentTarget)
63 |   })
64 | }
65 | 


--------------------------------------------------------------------------------
/src/components/business/create-design/index.ts:
--------------------------------------------------------------------------------
1 | import v from './createDesign.vue'
2 | export default v


--------------------------------------------------------------------------------
/src/components/business/image-cutout/ImageCutout/method.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-03 19:00:00
 4 |  * @Description: 裁剪组件公共方法
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @Date: 2024-03-03 19:00:00
 7 |  */
 8 | 
 9 | import Qiniu from '@/common/methods/QiNiu'
10 | import { TCommonUploadCb, TUploadErrorResult } from '@/api/ai'
11 | import { TImageCutoutState } from './index.vue'
12 | import api from '@/api'
13 | import { getImage } from '@/common/methods/getImgDetail'
14 | import _config from '@/config'
15 | import { Ref } from 'vue'
16 | 
17 | /** 选择图片 */
18 | export const selectImageFile = async (state: TImageCutoutState, raw: Ref<HTMLElement | null>, file: File, successCb?: (result: MediaSource, fileName: string) => void, uploadCb?: TCommonUploadCb) => {
19 |   // if (file.size > 1024 * 1024 * 2) {
20 |   //   alert('上传图片超出限制')
21 |   //   return false
22 |   // }
23 |   if (!raw.value) return
24 |   // 显示选择的图片
25 |   raw.value.addEventListener('load', () => {
26 |     state.offsetWidth = (raw.value as HTMLElement).offsetWidth
27 |   })
28 |   // TODO: 模拟演示
29 |   // state.rawImage = 'https://pic.imgdb.cn/item/66be4c1ed9c307b7e9f00b16.jpg' // URL.createObjectURL(file)
30 |   state.rawImage = 'https://s2.loli.net/2024/08/16/45aIdYbhgSefEoc.jpg'
31 | 
32 |   // 返回抠图结果
33 |   // const result = await api.ai.upload(file, (up: number, dp: number) => {
34 |   //   uploadCb && uploadCb(up, dp)
35 |   //   if (dp) {
36 |   //     state.progressText = dp === 100 ? '' : '导入中..'
37 |   //     state.progress = dp
38 |   //   } else {
39 |   //     state.progressText = up < 100 ? '上传中..' : '正在处理,请稍候..'
40 |   //     state.progress = up < 100 ? up : 0
41 |   //   }
42 |   // })
43 |   // if (typeof result == 'object' && (result as TUploadErrorResult).type !== 'application/json') {
44 |   // successCb && successCb(result as MediaSource, file.name)
45 |   // } else alert('服务器繁忙,请稍等下重新尝试~')
46 |   successCb('', file.name)
47 | }
48 | 
49 | export async function uploadCutPhotoToCloud(cutImage: string) {
50 |   try {
51 |     const response = await fetch(cutImage)
52 |     const buffer = await response.arrayBuffer()
53 |     const file = new File([buffer], `cut_image_${Math.random()}.png`)
54 |     // upload
55 |     const qnOptions = { bucket: 'xp-design', prePath: 'user' }
56 |     const result = await Qiniu.upload(file, qnOptions)
57 |     const { width, height } = await getImage(file)
58 |     const url = _config.IMG_URL + result.key
59 |     await api.material.addMyPhoto({ width, height, url })
60 |     return url
61 |   } catch (e) {
62 |     console.error(`upload cut file error: msg: ${e}`)
63 |     return ''
64 |   }
65 | }
66 | 


--------------------------------------------------------------------------------
/src/components/business/image-cutout/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-10-08 10:07:10
 4 |  * @Description: 图像抠图
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-07-12 00:05:48
 7 |  */
 8 | import index from './ImageCutout/index.vue'
 9 | export default index
10 | 


--------------------------------------------------------------------------------
/src/components/business/moveable/Selecto.ts:
--------------------------------------------------------------------------------
 1 | import Selecto from 'selecto'
 2 | import Moveable, { getElementInfo } from 'moveable'
 3 | // import store from '@/store'
 4 | import { useWidgetStore, useControlStore } from '@/store'
 5 | 
 6 | export default function(moveable: Moveable) {
 7 |   const widgetStore = useWidgetStore()
 8 |   const controlStore = useControlStore()
 9 |   const selecto = new Selecto({
10 |     container: document.getElementById('page-design'),
11 |     selectableTargets: ['.layer'],
12 |     selectByClick: false,
13 |     // 是否从内部目标中选择(default: true)
14 |     selectFromInside: false,
15 |     // 选择后,是否与所选目标一起选择下一个目标
16 |     continueSelect: false,
17 |     // Determines which key to continue selecting the next target via keydown and keyup.
18 |     toggleContinueSelect: 'shift',
19 |     // The container for keydown and keyup events
20 |     keyContainer: document.getElementById('page-design'),
21 |     // 目标与要选择的拖动区域重叠的速率。(默认:100)
22 |     hitRate: 5,
23 |     getElementRect: getElementInfo,
24 |   })
25 |   selecto.on('select', (e) => {
26 |     e.added.forEach((el) => {
27 |       if (!Array.from(el.classList).includes('layer-lock') && !el.hasAttribute('child')) {
28 |         el.classList.add('widget-selected')
29 |         widgetStore.selectWidgetsInOut({
30 |           uuid: el.getAttribute('data-uuid') || "",
31 |         })
32 |         // store.dispatch('selectWidgetsInOut', {
33 |         //   uuid: el.getAttribute('data-uuid'),
34 |         // })
35 |       }
36 |     })
37 |     e.removed.forEach((el) => {
38 |       el.classList.remove('widget-selected')
39 |       widgetStore.selectWidgetsInOut({
40 |         uuid: el.getAttribute('data-uuid') || "",
41 |       })
42 |       // store.dispatch('selectWidgetsInOut', {
43 |       //   uuid: el.getAttribute('data-uuid'),
44 |       // })
45 |     })
46 |     moveable.renderDirections = [] // ['nw', 'ne', 'sw', 'se'] // []
47 |     moveable.rotatable = false
48 |     moveable.target = [].slice.call(document.querySelectorAll('.widget-selected'))
49 |   }).on("dragStart", e => {
50 |     if (controlStore.dSpaceDown) {
51 |       e.stop();
52 |     }
53 |   })
54 | }
55 | 


--------------------------------------------------------------------------------
/src/components/business/moveable/style/index.less:
--------------------------------------------------------------------------------
 1 | // 2层 .zk-moveable-style 增加css权重层级,避免太多 important
 2 | .zk-moveable-style.zk-moveable-style {
 3 |   --moveable-color: #6ccfff;
 4 | 
 5 |   // 缩放圆点
 6 |   .moveable-control {
 7 |     background: #fff;
 8 |     box-sizing: border-box;
 9 |     display: block;
10 |     border: 1px solid #c0c5cf;
11 |     box-shadow: 0 0 2px 0 rgb(86, 90, 98, 0.2);
12 |     width: 12px;
13 |     height: 12px;
14 |     margin-top: -6px;
15 |     margin-left: -6px;
16 | 
17 |     // &.moveable-n,
18 |     // &.moveable-s,
19 |     // &.moveable-e,
20 |     // &.moveable-w {
21 |     //   display: none;
22 |     // }
23 | 
24 |     // 上下缩放点
25 |     &.moveable-n,
26 |     &.moveable-s {
27 |       width: 16px;
28 |       height: 8px;
29 |       margin-top: -4px;
30 |       margin-left: -8px;
31 |       border-radius: 6px;
32 |     }
33 |     // 左右缩放点
34 |     &.moveable-e,
35 |     &.moveable-w {
36 |       width: 8px;
37 |       height: 16px;
38 |       margin-left: -4px;
39 |       margin-top: -8px;
40 |       border-radius: 6px;
41 |     }
42 |   }
43 | 
44 |   // 旋转按钮
45 |   .moveable-rotation {
46 |     height: 35px;
47 |     display: block;
48 |     .moveable-rotation-control {
49 |       border: none;
50 |       background-image: url('./rotation-icon.svg');
51 |       width: 24px;
52 |       height: 24px;
53 |       background-size: 100% 100%;
54 |       display: block;
55 |       margin-left: -11px;
56 |       // margin-top: -11px;
57 |     }
58 | 
59 |     // 旋转的操作条
60 |     .moveable-rotation-line {
61 |       display: none;
62 |     }
63 |   }
64 | }
65 | 
66 | .moveable__remove-item {
67 |   position: fixed;
68 |   left: -9999px;
69 |   top: -9999px;
70 | }
71 | 


--------------------------------------------------------------------------------
/src/components/business/moveable/style/rotation-icon.svg:
--------------------------------------------------------------------------------
1 | <svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' fill='#757575'><g fill='none' fill-rule='evenodd'><circle stroke='#CCD1DA' fill='#FFF' cx='12' cy='12' r='11.5'/><path d='M16.242 12.012a4.25 4.25 0 00-5.944-4.158L9.696 6.48a5.75 5.75 0 018.048 5.532h1.263l-2.01 3.002-2.008-3.002h1.253zm-8.484-.004a4.25 4.25 0 005.943 3.638l.6 1.375a5.75 5.75 0 01-8.046-5.013H5.023L7.02 9.004l1.997 3.004h-1.26z' fill='#000' fill-rule='nonzero'/></g></svg>


--------------------------------------------------------------------------------
/src/components/business/picture-selector/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-10-08 10:07:10
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-06-26 20:48:29
 7 |  */
 8 | import index from './index.vue'
 9 | export default index
10 | 


--------------------------------------------------------------------------------
/src/components/business/qrcode/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-16 09:14:43
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-03-16 09:16:19
 7 |  */
 8 | import index from './index.vue'
 9 | export default index
10 | 


--------------------------------------------------------------------------------
/src/components/business/qrcode/index.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-16 09:15:52
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @Date: 2024-03-04 18:50:00
 7 | -->
 8 | <template>
 9 |   <div ref="qrCodeDom" class="qrcode__wrap"></div>
10 | </template>
11 | 
12 | <script lang="ts" setup>
13 | import { onMounted, ref, watch, nextTick } from 'vue'
14 | import QRCodeStyling, { Options } from 'qr-code-styling'
15 | import { debounce } from 'throttle-debounce'
16 | import { generateOption } from './method'
17 | 
18 | export type TQrcodeProps = {
19 |   width?: number
20 |   height?: number
21 |   image?: string
22 |   value?: string
23 |   dotsOptions: Options['dotsOptions']
24 | }
25 | 
26 | const props = withDefaults(defineProps<TQrcodeProps>(), {
27 |   width: 300,
28 |   height: 300,
29 |   dotsOptions: () => ({
30 |     color: '#41b583',
31 |     type: 'rounded',
32 |   })
33 | })
34 | 
35 | let options: Options = {}
36 | watch(
37 |   () => [props.width, props.height, props.dotsOptions],
38 |   () => {
39 |     render()
40 |   },
41 | )
42 | 
43 | const render = debounce(300, false, async () => {
44 |   options = generateOption(props)
45 |   if (props.value) {
46 |     options && qrCode.update(options)
47 |     await nextTick()
48 |     if (!qrCodeDom?.value?.firstChild) return
49 |     (qrCodeDom.value.firstChild as HTMLElement).setAttribute('style', "width: 100%;") // 强制其适应缩放
50 |   }
51 | })
52 | 
53 | const qrCode = new QRCodeStyling(options)
54 | const qrCodeDom = ref<HTMLElement>()
55 | 
56 | onMounted(() => {
57 |   render()
58 |   qrCode.append(qrCodeDom.value)
59 | })
60 | 
61 | </script>
62 | 


--------------------------------------------------------------------------------
/src/components/business/qrcode/method.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-04 18:10:00
 4 |  * @Description:  
 5 |  * @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @Date: 2024-03-04 18:10:00
 7 |  */
 8 | import { CornerDotType, Options } from "qr-code-styling"
 9 | import { TQrcodeProps } from "./index.vue"
10 | 
11 | /** 生成二维码数据 */
12 | export function generateOption(props: TQrcodeProps): Options {
13 |   return {
14 |     width: props.width,
15 |     height: props.height,
16 |     type: 'canvas', // canvas svg
17 |     data: props.value,
18 |     image: props.image, // /favicon.svg
19 |     margin: 0,
20 |     qrOptions: {
21 |       typeNumber: 3,
22 |       mode: 'Byte',
23 |       errorCorrectionLevel: 'M',
24 |     },
25 |     imageOptions: {
26 |       hideBackgroundDots: true,
27 |       imageSize: 0.4,
28 |       margin: 6,
29 |       crossOrigin: 'anonymous',
30 |     },
31 |     backgroundOptions: {
32 |       color: '#ffffff',
33 |     },
34 |     dotsOptions: {
35 |       // color: '#41b583',
36 |       // type: 'rounded' as DotType,
37 |       ...props.dotsOptions,
38 |     },
39 |     cornersSquareOptions: {
40 |       color: props.dotsOptions?.color,
41 |       // type: '',
42 |       // type: 'extra-rounded' as CornerSquareType,
43 |       // gradient: {
44 |       //   type: 'linear', // 'radial'
45 |       //   rotation: 180,
46 |       //   colorStops: [{ offset: 0, color: '#25456e' }, { offset: 1, color: '#4267b2' }]
47 |       // },
48 |     },
49 |     cornersDotOptions: {
50 |       color: props.dotsOptions?.color,
51 |       type: 'square' as CornerDotType,
52 |       // gradient: {
53 |       //   type: 'linear', // 'radial'
54 |       //   rotation: 180,
55 |       //   colorStops: [{ offset: 0, color: '#00266e' }, { offset: 1, color: '#4060b3' }]
56 |       // },
57 |     },
58 |   }
59 | }
60 | 
61 | 


--------------------------------------------------------------------------------
/src/components/business/right-click-menu/rcMenuData.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-07-30 17:38:50
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @Date: 2024-03-04 18:50:00
 7 |  */
 8 | 
 9 | export type TMenuItemData = {
10 |   left: number
11 |   top: number
12 |   list: TWidgetItemData[]
13 | }
14 | 
15 | export const menuList: TMenuItemData = {
16 |   left: 0,
17 |   top: 0,
18 |   list: [],
19 | }
20 | 
21 | export type TWidgetItemData = {
22 |   type: 'copy' | 'paste' | 'index-up' | 'index-down' | 'del' | 'ungroup'
23 |   text: string
24 | }
25 | 
26 | export const widgetMenu: TWidgetItemData[] = [
27 |   {
28 |     type: 'copy',
29 |     text: '复制',
30 |   },
31 |   {
32 |     type: 'paste',
33 |     text: '粘贴',
34 |   },
35 |   {
36 |     type: 'index-up',
37 |     text: '上移一层',
38 |   },
39 |   {
40 |     type: 'index-down',
41 |     text: '下移一层',
42 |   },
43 |   {
44 |     type: 'del',
45 |     text: '删除',
46 |   },
47 | ]
48 | 
49 | export const pageMenu: TWidgetItemData[] = [
50 |   {
51 |     type: 'paste',
52 |     text: '粘贴',
53 |   },
54 | ]
55 | 


--------------------------------------------------------------------------------
/src/components/common/PopoverTip.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-04-10 12:12:57
 4 |  * @Description: tooltip提示
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-03-11 01:41:20
 7 | -->
 8 | <template>
 9 |   <el-popover ref="popover" :placement="position" :title="title" :width="width" trigger="hover" :content="content">
10 |     <template #reference>
11 |       <slot />
12 |     </template>
13 |   </el-popover>
14 | </template>
15 | 
16 | <script lang="ts" setup>
17 | type TProps = {
18 |   title: string
19 |   width: number
20 |   content: string
21 |   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"
22 |   offset?: number
23 | }
24 | 
25 | const { title, width, content, position } = withDefaults(defineProps<TProps>(), {
26 |   title: '',
27 |   width: 0,
28 |   content: '',
29 |   position: 'bottom'
30 | })
31 | </script>
32 | 


--------------------------------------------------------------------------------
/src/components/common/ProgressLoading/index.vue:
--------------------------------------------------------------------------------
  1 | <template>
  2 |   <div v-if="percent" class="mask">
  3 |     <div class="content">
  4 |       <div class="text">{{ text }}</div>
  5 |       <el-progress style="width: 100%" :text-inside="true" :percentage="percent" />
  6 |       <div class="text btn" @click="cancel">{{ cancelText }}</div>
  7 |       <div class="text info">{{ msg }}</div>
  8 |     </div>
  9 |   </div>
 10 | </template>
 11 | 
 12 | <script lang="ts" setup>
 13 | import { watch } from 'vue'
 14 | import { ElProgress } from 'element-plus'
 15 | 
 16 | type TProps = {
 17 |   percent: number
 18 |   text?: string
 19 |   cancelText?: string
 20 |   msg?: string
 21 | }
 22 | 
 23 | type TEmits = {
 24 |   (event: 'done'): void
 25 |   (event: 'cancel'): void
 26 | }
 27 | 
 28 | const props = withDefaults(defineProps<TProps>(), {
 29 |   percent: 0,
 30 |   text: '',
 31 |   cancelText: '',
 32 |   msg: ''
 33 | })
 34 | 
 35 | const emit = defineEmits<TEmits>()
 36 | 
 37 | watch(
 38 |   () => props.percent,
 39 |   (num) => {
 40 |     if (num >= 100) {
 41 |       setTimeout(() => {
 42 |         emit('done')
 43 |       }, 1000)
 44 |     }
 45 |   },
 46 | )
 47 | 
 48 | const cancel = () => {
 49 |   emit('cancel')
 50 | }
 51 | 
 52 | defineExpose({
 53 |   cancel
 54 | })
 55 | 
 56 | </script>
 57 | 
 58 | <style lang="less" scoped>
 59 | :deep(.el-progress-bar__innerText) {
 60 |   opacity: 0;
 61 | }
 62 | .mask {
 63 |   display: flex;
 64 |   justify-content: center;
 65 |   flex-direction: column;
 66 |   padding: 0 24%;
 67 |   width: 100%;
 68 |   height: 100%;
 69 |   position: fixed;
 70 |   z-index: 99999;
 71 |   top: 0;
 72 |   left: 0;
 73 |   background: rgba(0, 0, 0, 0.7);
 74 | }
 75 | .content {
 76 |   background: #ffffff;
 77 |   border-radius: 8px;
 78 |   padding: 2rem 4rem;
 79 | }
 80 | .text {
 81 |   margin: 2rem 0;
 82 |   font-size: 20px;
 83 |   font-weight: bold;
 84 |   width: 100%;
 85 |   text-align: center;
 86 |   color: #333333;
 87 | }
 88 | .btn {
 89 |   font-weight: 400;
 90 |   font-size: 16px;
 91 |   cursor: pointer;
 92 |   color: #3771e5;
 93 | }
 94 | .info {
 95 |   font-weight: 400;
 96 |   font-size: 16px;
 97 |   color: #777777;
 98 | }
 99 | </style>
100 | 


--------------------------------------------------------------------------------
/src/components/common/Uploader/index.ts:
--------------------------------------------------------------------------------
1 | import upload from './index.vue'
2 | 
3 | export default upload
4 | 


--------------------------------------------------------------------------------
/src/components/modules/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-07-13 22:51:29
 4 |  * @Description: Widgets、panel中所有组件将会自动全局引入
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-09 22:31:08
 7 |  */
 8 | import { App } from "vue"
 9 | 
10 | function capitalizeFirstLetter(string: string) {
11 |   return string.charAt(0).toUpperCase() + string.slice(1)
12 | }
13 | 
14 | // 排除要全局引入的组件,可以是目录名也可以是文件名
15 | const exclude = ['settings', 'layout']
16 | 
17 | const regex = RegExp('.*^(?!.*?(' + exclude.join('|') + ')).*\\.vue
#39;)
18 | 
19 | // const requireComponent = require.context('.', true, /\.vue$/) // 找到components文件夹下以.vue命名的文件
20 | 
21 | const requireComponent = import.meta.glob('./**/*.vue', { eager: true })
22 | 
23 | function guide(Vue: App) {
24 |   for (const fileName in requireComponent) {
25 |     if (regex.test(fileName)) {
26 |       const componentConfig = requireComponent[fileName]
27 |       const componentName = capitalizeFirstLetter(fileName.replace(/^\..*\//, '').replace(/\.\w+$/, ''))
28 |       Vue.component(componentName, componentConfig.default || componentConfig)
29 |     }
30 |   }
31 | }
32 | 
33 | export default guide
34 | 


--------------------------------------------------------------------------------
/src/components/modules/layout/designBoard/comps/pageWatermark.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-08 19:19:17
 4 |  * @Description: 水印组件封装
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-08 21:30:12
 7 | -->
 8 | <template>
 9 |   <slot v-if="isDrawPage" />
10 |   <el-watermark v-else :style="props.customStyle" :gap="[140, 120]" :content="watermark">
11 |     <slot />
12 |   </el-watermark>
13 | </template>
14 | 
15 | <script setup lang="ts">
16 | import { computed } from 'vue'
17 | import { ElWatermark } from 'element-plus'
18 | import { storeToRefs } from 'pinia'
19 | import { useBaseStore } from '@/store'
20 | import { useRoute } from 'vue-router'
21 | const route = useRoute()
22 | 
23 | type TProps = {
24 |   customStyle: any
25 | }
26 | 
27 | const props = withDefaults(defineProps<TProps>(), {
28 |   customStyle: {}
29 | })
30 | 
31 | const isDrawPage = computed(() => route.name === 'Draw')
32 | const baseStore = useBaseStore()
33 | const { watermark } = storeToRefs(baseStore)
34 | </script>
35 | 


--------------------------------------------------------------------------------
/src/components/modules/layout/multipleBoards/index.ts:
--------------------------------------------------------------------------------
1 | import index from './multipleBoards.vue'
2 | export default index


--------------------------------------------------------------------------------
/src/components/modules/layout/zoomControl/data.ts:
--------------------------------------------------------------------------------
 1 | 
 2 | export type TZoomData = {
 3 |   text: string
 4 |   value: number
 5 | }
 6 | 
 7 | export const ZoomList: TZoomData[] = [
 8 |   {
 9 |     text: '25%',
10 |     value: 25,
11 |   },
12 |   {
13 |     text: '50%',
14 |     value: 50,
15 |   },
16 |   {
17 |     text: '75%',
18 |     value: 75,
19 |   },
20 |   {
21 |     text: '100%',
22 |     value: 100,
23 |   },
24 |   {
25 |     text: '125%',
26 |     value: 125,
27 |   },
28 |   {
29 |     text: '150%',
30 |     value: 150,
31 |   },
32 |   {
33 |     text: '200%',
34 |     value: 200,
35 |   },
36 |   {
37 |     text: '适应屏幕',
38 |     value: -1,
39 |     // icon: 'icon-best-size',
40 |   },
41 | ]
42 | 
43 | 
44 | export const OtherList: TZoomData[] = [
45 |   {
46 |     text: '250%',
47 |     value: 250,
48 |   },
49 |   {
50 |     text: '300%',
51 |     value: 300,
52 |   },
53 |   {
54 |     text: '350%',
55 |     value: 350,
56 |   },
57 |   {
58 |     text: '400%',
59 |     value: 400,
60 |   },
61 |   {
62 |     text: '450%',
63 |     value: 450,
64 |   },
65 |   {
66 |     text: '500%',
67 |     value: 500,
68 |   },
69 | ]
70 | 


--------------------------------------------------------------------------------
/src/components/modules/panel/types/wrap.d.ts:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | type TCommonImgListData = {
 4 |   isDelect: boolean
 5 |   cover: string
 6 |   fail: boolean
 7 |   top: number
 8 |   left: number
 9 |   width: number
10 |   height: number
11 |   title: string
12 | }
13 | 
14 | type TCommonPhotoListData = {
15 |   listWidth: number
16 |   gap: number
17 |   thumb?: string
18 |   url: string
19 |   color: string
20 |   isDelect: boolean
21 |   width: number
22 |   height: number
23 |   model: string
24 | }
25 | 


--------------------------------------------------------------------------------
/src/components/modules/panel/wrap/components/editModel.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-01-11 17:54:14
 4 |  * @Description: 模板编辑组件
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-02-11 00:07:36
 7 | -->
 8 | <template>
 9 |   <div class="wrap">
10 |     <slot />
11 |     <div class="showMask" @click.stop="">
12 |       <el-dropdown placement="bottom-end" :show-arrow="false">
13 |         <i class="iconfont icon-more"></i>
14 |         <template #dropdown>
15 |           <el-dropdown-menu>
16 |             <el-dropdown-item v-for="(op, oi) in options" :key="oi + 'o'" @click="op.fn(data)">{{ op.name }}</el-dropdown-item>
17 |           </el-dropdown-menu>
18 |         </template>
19 |       </el-dropdown>
20 |     </div>
21 |   </div>
22 | </template>
23 | 
24 | <script lang="ts">
25 | import { defineComponent } from 'vue'
26 | import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
27 | import useConfirm from '@/common/methods/confirm'
28 | 
29 | export default defineComponent({
30 |   components: { ElDropdown, ElDropdownItem, ElDropdownMenu },
31 |   props: {
32 |     options: {
33 |       default: () => [],
34 |     },
35 |     data: {},
36 |   },
37 |   emits: ['action'],
38 |   setup(props, context) {
39 |     async function action(name: string, value: any) {
40 |       if (name === 'del') {
41 |         const isDel = await useConfirm('警告', '删除后不可恢复,是否继续', 'warning')
42 |         if (!isDel) {
43 |           return false
44 |         }
45 |       }
46 |       context.emit('action', { name, value })
47 |     }
48 |     return {
49 |       action,
50 |     }
51 |   },
52 | })
53 | </script>
54 | 
55 | <style lang="less" scoped>
56 | .wrap {
57 |   width: 100%;
58 |   height: 100%;
59 |   position: relative;
60 | }
61 | .wrap:hover > .showMask {
62 |   opacity: 1;
63 | }
64 | .showMask {
65 |   cursor: grab;
66 |   opacity: 0;
67 |   background: rgba(0, 0, 0, 0.4);
68 |   position: absolute;
69 |   z-index: 2;
70 |   border-radius: 0.7rem;
71 |   top: 0;
72 |   right: 0;
73 | 
74 |   .iconfont {
75 |     color: #ffffff;
76 |     margin: 2px 10px;
77 |   }
78 | }
79 | </style>
80 | 


--------------------------------------------------------------------------------
/src/components/modules/panel/wrap/components/imageTip.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-04 19:12:40
 4 |  * @Description: 图片描述ToolTip
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @Date: 2024-03-06 21:16:00
 7 | -->
 8 | <template>
 9 |   <el-tooltip :disabled="!detail.author" :offset="1" effect="light" placement="bottom-start" :hide-after="0" :enterable="false" raw-content>
10 |     <template #content>
11 |       <p style="max-width: 140px">
12 |         <b>{{ detail.description }}</b>
13 |       </p>
14 |       <p>@{{ detail.author }}</p>
15 |     </template>
16 |     <slot />
17 |   </el-tooltip>
18 | </template>
19 | 
20 | <script lang="ts" setup>
21 | 
22 | export type TImageTipDetailData = {
23 |   author: string
24 |   description: string
25 | }
26 | 
27 | type Tprops = {
28 |   detail: TImageTipDetailData
29 | }
30 | 
31 | const { detail } = defineProps<Tprops>()
32 | 
33 | </script>
34 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wGroup/groupSetting.ts:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | export const wGroupSetting = {
 4 |   name: '组合',
 5 |   type: 'w-group',
 6 |   uuid: -1,
 7 |   width: 0,
 8 |   height: 0,
 9 |   left: 0,
10 |   top: 0,
11 |   transform: '',
12 |   opacity: 1,
13 |   parent: '-1',
14 |   isContainer: true,
15 |   record: {
16 |     width: 0,
17 |     height: 0,
18 |     minWidth: 0,
19 |     minHeight: 0,
20 |     dir: 'none',
21 |   },
22 | }
23 | 
24 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wGroup/wGroupStatic.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-02 09:41:41
 4 |  * @Description: 静态组件
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-16 16:13:56
 7 | -->
 8 | <template>
 9 |   <div
10 |     :style="{
11 |       position: 'absolute',
12 |       left: (props.params.left || 0) - (props.parent?.left || 0) + 'px',
13 |       top: (props.params.top || 0) - (props.parent.top || 0) + 'px',
14 |       width: params.width + 'px',
15 |       height: params.height + 'px',
16 |       opacity: params.opacity,
17 |     }"
18 |   >
19 |     <slot></slot>
20 |   </div>
21 | </template>
22 | 
23 | <script lang="ts" setup>
24 | // 组合组件
25 | 
26 | export type TParamsData = {
27 |   left: number
28 |   top: number
29 |   width: number
30 |   height: number
31 |   opacity: number
32 |   rotate: number
33 |   uuid: string
34 |   lock: boolean
35 |   fontSize: number
36 | }
37 | 
38 | type TProps = {
39 |   params?: Partial<TParamsData>
40 |   parent?: Partial<Pick<TParamsData, "top" | "left">>
41 | }
42 | const props = withDefaults(defineProps<TProps>(), {
43 |   params: () => ({}),
44 |   parent: () => ({})
45 | })
46 | 
47 | </script>
48 | 
49 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wImage/components/innerToolBar.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-09-27 15:08:54
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2022-09-27 15:52:07
 7 | -->
 8 | <template>
 9 |   <div class="inner-tool-bar">
10 |     <slot />
11 |   </div>
12 | </template>
13 | 
14 | <style lang="less" scoped>
15 | .inner-tool-bar {
16 |   position: fixed;
17 |   z-index: 999999;
18 |   margin-top: -57px;
19 | 
20 |   padding: 5px;
21 |   font-size: 13px;
22 |   background: #fff;
23 |   border-radius: 3px;
24 |   box-shadow: 0 0 5px rgb(0 0 0 / 30%);
25 |   display: flex;
26 |   align-items: center;
27 | }
28 | </style>
29 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wImage/wImageSetting.ts:
--------------------------------------------------------------------------------
 1 | export type TImageSetting = {
 2 |   name: string
 3 |   type: string
 4 |   uuid: string
 5 |   width: number
 6 |   height: number
 7 |   left: number
 8 |   top: number
 9 |   zoom: number
10 |   transform: string
11 |   radius: number
12 |   opacity: number
13 |   parent: string
14 |   imgUrl: string
15 |   mask: string
16 |   setting: [],
17 |   rotate: number
18 |   record: {
19 |     width: number
20 |     height: number
21 |     minWidth: number
22 |     minHeight: number
23 |     dir: string
24 |   },
25 |   lock: false,
26 |   isNinePatch: false,
27 |   flip: string | null
28 |   sliceData: {
29 |     ratio: number
30 |     left: number
31 |   }
32 |   cropEdit?: boolean
33 | }
34 | 
35 | const setting: TImageSetting = {
36 |   name: '图片',
37 |   type: 'w-image',
38 |   uuid: '-1',
39 |   width: 300,
40 |   height: 300,
41 |   left: 0,
42 |   top: 0,
43 |   zoom: 1,
44 |   transform: '',
45 |   radius: 0,
46 |   opacity: 1,
47 |   parent: '-1',
48 |   imgUrl: '',
49 |   mask: '',
50 |   setting: [],
51 |   rotate: 0,
52 |   record: {
53 |     width: 0,
54 |     height: 0,
55 |     minWidth: 10,
56 |     minHeight: 10,
57 |     dir: 'all',
58 |   },
59 |   lock: false,
60 |   isNinePatch: false,
61 |   flip: '',
62 |   sliceData: {
63 |     ratio: 0,
64 |     left: 0,
65 |   }
66 | }
67 | 
68 | export default setting
69 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wImage/wImageStatic.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-12 17:47:19
 4 |  * @Description: 静态组件
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-16 16:25:35
 7 | -->
 8 | <template>
 9 |   <div
10 |     ref="widgetRef"
11 |     :style="{
12 |       position: state.position,
13 |       left: params.left - parent.left + 'px',
14 |       top: params.top - parent.top + 'px',
15 |       width: params.width + 'px',
16 |       height: params.height + 'px',
17 |       opacity: params.opacity,
18 |     }"
19 |   >
20 |     <div :style="{ transform: params.flip ? `rotate${params.flip}(180deg)` : undefined, borderRadius: params.radius + 'px', '-webkit-mask-image': `${params.mask ? `url('${params.mask}')` : 'initial'}` }" :class="['img__box', { mask: params.mask }]">
21 |       <div v-if="params.isNinePatch" ref="targetRef" class="target" :style="{ border: `${(params.height * params.sliceData.ratio) / 2}px solid transparent`, borderImage: `url('${params.imgUrl}') ${params.sliceData.left} round` }"></div>
22 |       <img v-else ref="targetRef" class="target" style="transform-origin: center" :src="params.imgUrl" />
23 |     </div>
24 |   </div>
25 | </template>
26 | 
27 | <script lang="ts" setup>
28 | import { CSSProperties, reactive, ref } from 'vue'
29 | import setting from "./wImageSetting"
30 | 
31 | type TProps = {
32 |   params: typeof setting
33 |   parent: {
34 |     left: number
35 |     top: number
36 |   }
37 | }
38 | 
39 | type TState = {
40 |   position: 'absolute' | 'relative', // 'absolute'relative
41 |   editBoxStyle: CSSProperties,
42 |   cropWidgetXY: {
43 |     x: number
44 |     y: number
45 |   }
46 |   holdPosition: {
47 |     left: number
48 |     top: number
49 |   }
50 | }
51 | 
52 | const props = defineProps<TProps>()
53 | const state = reactive<TState>({
54 |   position: 'absolute', // 'absolute'relative
55 |   editBoxStyle: {
56 |     transformOrigin: 'center',
57 |     transform: '',
58 |   },
59 |   cropWidgetXY: {
60 |     x: 0,
61 |     y: 0,
62 |   },
63 |   holdPosition: {
64 |     left: 0,
65 |     top: 0,
66 |   }
67 | })
68 | 
69 | const widgetRef = ref<HTMLElement | null>(null)
70 | const targetRef = ref<HTMLImageElement | null>(null)
71 | 
72 | </script>
73 | 
74 | <style lang="less" scoped>
75 | .mask {
76 |   -webkit-mask-size: 100% 100%;
77 |   -webkit-mask-position: center;
78 | }
79 | .img__box {
80 |   width: 100%;
81 |   height: 100%;
82 |   overflow: hidden;
83 |   img {
84 |     display: block;
85 |   }
86 | }
87 | .target {
88 |   display: block;
89 |   width: 100%;
90 |   height: 100%;
91 | }
92 | </style>
93 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wQrcode/wQrcodeSetting.ts:
--------------------------------------------------------------------------------
 1 | import { DotType } from "qr-code-styling"
 2 | 
 3 | export type TWQrcodeSetting = {
 4 |   name: string
 5 |   type: string
 6 |   uuid: string | number
 7 |   width: number
 8 |   height: number
 9 |   left: number
10 |   top: number
11 |   zoom: number
12 |   transform: string
13 |   radius: number
14 |   opacity: number
15 |   parent: string
16 |   url: string
17 |   dotType: DotType
18 |   dotColorType: string
19 |   dotRotation: number
20 |   dotColor: string
21 |   dotColor2: string
22 |   value: string
23 |   setting: Record<string, any>[]
24 |   record: {
25 |     width: number
26 |     height: number
27 |     minWidth: number
28 |     minHeight: number
29 |     dir: string
30 |   }
31 |   cropEdit?: boolean
32 | }
33 | 
34 | export const wQrcodeSetting: TWQrcodeSetting = {
35 |   name: '二维码',
36 |   type: 'w-qrcode',
37 |   uuid: -1,
38 |   width: 300,
39 |   height: 300,
40 |   left: 0,
41 |   top: 0,
42 |   zoom: 1,
43 |   transform: '',
44 |   radius: 0,
45 |   opacity: 1,
46 |   parent: '-1',
47 |   url: '',
48 |   dotType: 'classy',
49 |   dotColorType: 'single',
50 |   dotRotation: 270,
51 |   dotColor: '#35495E',
52 |   dotColor2: '#35495E',
53 |   value: 'https://xp.palxp.cn',
54 |   setting: [],
55 |   record: {
56 |     width: 0,
57 |     height: 0,
58 |     minWidth: 10,
59 |     minHeight: 10,
60 |     dir: 'all',
61 |   },
62 | }
63 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wSvg/wSvgSetting.ts:
--------------------------------------------------------------------------------
 1 | 
 2 | export type TWSvgSetting = {
 3 |   name: string,
 4 |   type: string,
 5 |   uuid: string
 6 |   width: number
 7 |   height: number
 8 |   colors: [],
 9 |   left: number
10 |   top: number
11 |   // zoom: 1.5,
12 |   transform: string
13 |   radius: number
14 |   opacity: number
15 |   parent: string
16 |   svgUrl: string
17 |   setting: [],
18 |   record: {
19 |     width: number
20 |     height: number
21 |     minWidth: number
22 |     minHeight: number
23 |   },
24 |   zoom?: number
25 |   cropEdit?: boolean
26 |   imgUrl?: string
27 |   rotate?: number
28 |   x?: number
29 |   y?: number
30 | }
31 | 
32 | export const wSvgSetting = {
33 |   name: '矢量图形',
34 |   type: "w-svg",
35 |   uuid: `-1`,
36 |   width: 100,
37 |   height: 100,
38 |   colors: [],
39 |   left: 0,
40 |   top: 0,
41 |   // zoom: 1.5,
42 |   transform: '',
43 |   radius: 0,
44 |   opacity: 1,
45 |   parent: '-1',
46 |   svgUrl: '',
47 |   setting: [],
48 |   record: {
49 |     width: 0,
50 |     height: 0,
51 |     minWidth: 10,
52 |     minHeight: 10,
53 |   },
54 | }
55 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wText/getGradientOrImg.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-11-29 11:00:41
 4 |  * @Description: 处理字体填充特效
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-11-29 11:01:50
 7 |  */
 8 | export default (effect: any) => {
 9 |   let result = ''
10 |   switch (Number(effect.filling.type)) {
11 |     case 2:
12 |       {
13 |         const { angle, stops } = effect.filling.gradient
14 |         const gradients = stops.map((x: any) => `${x.color} ${Number(x.offset) * 100}%`)
15 |         result = `linear-gradient(${angle}deg, ${gradients.toString()})`
16 |       }
17 |       break
18 |     case 1:
19 |       result = `url(${effect.filling.imageContent.image})`
20 |       break
21 |     default:
22 |       result = effect.filling.color
23 |       break
24 |   }
25 |   return result
26 | }
27 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wText/pageFontsFilter.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-10-14 20:16:48
 4 |  * @Description: 找出页面中使用的字体
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-10-14 20:29:26
 7 |  */
 8 | // import store from '@/store'
 9 | import { useWidgetStore } from '@/store'
10 | import { toRaw } from 'vue'
11 | export default () => {
12 |   const widgetStore = useWidgetStore()
13 |   const collector = new Set<string>()
14 |   const fonts: Record<string, any> = {}
15 |   const { dWidgets: widgets } = widgetStore
16 |   for (let i = 0; i < widgets.length; i++) {
17 |     const { type, fontClass } = widgets[i]
18 |     if (type === 'w-text') {
19 |       fontClass && collector.add(fontClass.id)
20 |       fontClass && (fonts[fontClass.id] = toRaw(fontClass))
21 |     }
22 |   }
23 |   return Array.from(collector).map((id: string) => fonts[id])
24 | }
25 | 


--------------------------------------------------------------------------------
/src/components/modules/widgets/wText/wTextSetting.ts:
--------------------------------------------------------------------------------
  1 | import { StyleValue } from "vue"
  2 | 
  3 | export type TwTextData = {
  4 |   name: string
  5 |   type: string
  6 |   uuid: number
  7 |   editable: boolean,
  8 |   left: number
  9 |   top: number
 10 |   transform: string
 11 |   lineHeight: number
 12 |   letterSpacing: number
 13 |   fontSize: number
 14 |   zoom: number
 15 |   fontClass: {
 16 |     alias: string
 17 |     id: number
 18 |     value: string
 19 |     url: string
 20 |   },
 21 |   fontFamily: string
 22 |   fontWeight: string
 23 |   fontStyle: string
 24 |   writingMode: StyleProperty.WritingMode
 25 |   textDecoration: string
 26 |   color: string
 27 |   textAlign: StyleProperty.TextAlign
 28 |   textAlignLast: StyleProperty.TextAlign
 29 |   text: string
 30 |   opacity: number
 31 |   backgroundColor: string
 32 |   parent: string
 33 |   record: {
 34 |     width: number
 35 |     height: number
 36 |     minWidth: number
 37 |     minHeight: number
 38 |     dir: string
 39 |   },
 40 |   textEffects?: {
 41 |     filling: {
 42 |       enable: boolean
 43 |       type: number
 44 |       color: string
 45 |     }
 46 |     stroke: {
 47 |       enable: boolean
 48 |       width: number
 49 |       color: string
 50 |     }
 51 |     shadow: {
 52 |       enable: boolean
 53 |       offsetY: number
 54 |       offsetX: number
 55 |       blur: number
 56 |       color: string
 57 |     }
 58 |     offset: {
 59 |       enable: boolean
 60 |       x: number
 61 |       y: number
 62 |     }
 63 |   }[]
 64 |   width?: number
 65 |   height?: number
 66 |   degree?: number
 67 | }
 68 | 
 69 | export const wTextSetting: TwTextData = {
 70 |   name: '文本',
 71 |   type: 'w-text',
 72 |   uuid: -1,
 73 |   editable: false,
 74 |   left: 0,
 75 |   top: 0,
 76 |   transform: '',
 77 |   lineHeight: 1.5,
 78 |   letterSpacing: 0,
 79 |   fontSize: 24,
 80 |   zoom: 1,
 81 |   fontClass: {
 82 |     alias: '站酷快乐体',
 83 |     id: 543,
 84 |     value: 'zcool-kuaile-regular',
 85 |     url: 'https://lib.baomitu.com/fonts/zcool-kuaile/zcool-kuaile-regular.woff2',
 86 |   },
 87 |   fontFamily: 'SourceHanSansSC-Regular',
 88 |   fontWeight: 'normal',
 89 |   fontStyle: 'normal',
 90 |   writingMode: 'horizontal-tb',
 91 |   textDecoration: 'none',
 92 |   color: '#000000ff',
 93 |   textAlign: 'left',
 94 |   text: '',
 95 |   opacity: 1,
 96 |   backgroundColor: '',
 97 |   parent: '-1',
 98 |   record: {
 99 |     width: 0,
100 |     height: 0,
101 |     minWidth: 0,
102 |     minHeight: 0,
103 |     dir: 'horizontal',
104 |   },
105 | }


--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-05 07:31:45
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 05:30:15
 7 |  */
 8 | // const prefix = import.meta.env
 9 | const prefix = process.env
10 | 
11 | const isDev = prefix.NODE_ENV === 'development'
12 | import { version } from '../package.json'
13 | 
14 | export default {
15 |   isDev,
16 |   BASE_URL: isDev ? '/' : './',
17 |   VERSION: version,
18 |   APP_NAME: '迅排设计',
19 |   COPYRIGHT: 'ShawnPhang - Design.pPalxp.cn',
20 |   API_URL: isDev ? 'http://localhost:7001' : '', // 后端地址
21 |   SCREEN_URL: isDev ? 'http://localhost:7001' : '', // 截图服务地址
22 |   IMG_URL: 'https://store.palxp.cn/', // 七牛云资源地址
23 |   // ICONFONT_URL: '//at.alicdn.com/t/font_3223711_74mlzj4jdue.css',
24 |   ICONFONT_URL: '//at.alicdn.com/t/font_2717063_ypy8vprc3b.css?display=swap',
25 |   ICONFONT_EXTRA: '//at.alicdn.com/t/c/font_3228074_xojoer6zhp.css',
26 |   QINIUYUN_PLUGIN: 'https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/qiniu-js/2.5.5/qiniu.min.js',
27 |   supportSubFont: false, // 是否开启服务端字体压缩
28 | }
29 | 
30 | export const LocalStorageKey = {
31 |   tokenKey: "xp_token"
32 | }
33 | 


--------------------------------------------------------------------------------
/src/languages/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-19 04:14:02
 4 |  * @Description: i18n 示例
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-05-19 05:19:57
 7 |  */
 8 | import { createI18n } from 'vue-i18n'
 9 | 
10 | import zh from './modules/zh'
11 | import en from './modules/en'
12 | 
13 | const i18n = createI18n({
14 |   // Use Composition API, Set to false
15 |   allowComposition: true,
16 |   legacy: false,
17 |   locale: getBrowserLang(),
18 |   messages: {
19 |     zh,
20 |     en,
21 |   },
22 | })
23 | 
24 | /**
25 |  * @description 获取浏览器默认语言
26 |  * @returns {String}
27 |  */
28 | export function getBrowserLang() {
29 |   let browserLang = navigator.language ? navigator.language : navigator.browserLanguage
30 |   let defaultBrowserLang = ''
31 |   if (['cn', 'zh', 'zh-cn'].includes(browserLang.toLowerCase())) {
32 |     defaultBrowserLang = 'zh'
33 |   } else {
34 |     defaultBrowserLang = 'en'
35 |   }
36 |   return defaultBrowserLang
37 | }
38 | 
39 | export default i18n
40 | 


--------------------------------------------------------------------------------
/src/languages/modules/en/header.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-19 05:14:04
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-05-19 05:14:30
 7 |  */
 8 | export default {
 9 |   logout: 'Logout',
10 |   save: 'Save',
11 |   download: 'Download',
12 | }
13 | 


--------------------------------------------------------------------------------
/src/languages/modules/en/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-19 04:14:31
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-05-19 05:17:29
 7 |  */
 8 | import header from "./header"
 9 | export default {
10 |   home: {
11 |     welcome: 'Welcome',
12 |   },
13 |   tabs: {
14 |     refresh: 'Refresh',
15 |   },
16 |   header
17 | }
18 | 


--------------------------------------------------------------------------------
/src/languages/modules/zh/header.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-19 05:14:10
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 15:52:36
 7 |  */
 8 | export default {
 9 |   logout: '退出登录',
10 |   save: '保存',
11 |   download: '下载模版',
12 | }
13 | 


--------------------------------------------------------------------------------
/src/languages/modules/zh/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-05-19 04:14:35
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-05-19 05:17:16
 7 |  */
 8 | import header from './header'
 9 | export default {
10 |   home: {
11 |     welcome: '欢迎使用',
12 |   },
13 |   tabs: {
14 |     refresh: '刷新',
15 |   },
16 |   header,
17 | }
18 | 


--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
 1 | import { createApp } from 'vue'
 2 | import App from './App.vue'
 3 | import router from './router'
 4 | import utils from './utils'
 5 | import 'normalize.css/normalize.css'
 6 | import '@/assets/styles/index.less'
 7 | import elementConfig from './utils/widgets/elementConfig'
 8 | import { createPinia } from 'pinia'
 9 | import I18n from '@/languages/index'
10 | 
11 | const pinia = createPinia()
12 | const app = createApp(App)
13 | 
14 | elementConfig.components.forEach((component) => {
15 |   app.component(component.name, component)
16 | })
17 | 
18 | elementConfig.plugins.forEach((plugin) => {
19 |   app.use(plugin)
20 | })
21 | 
22 | app
23 |   // .use(store)
24 |   .use(pinia)
25 |   .use(router)
26 |   .use(utils)
27 |   .use(I18n)
28 |   .mount('#app')
29 | 


--------------------------------------------------------------------------------
/src/mixins/move.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-19 18:43:22
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-16 19:10:18
 7 |  */
 8 | import { useControlStore, useWidgetStore } from '@/store'
 9 | // import store from '@/store'
10 | 
11 | const move = {
12 |   methods: {
13 |     initmovement(e: any) {
14 |       // let target = store.state.pageDesign.dActiveElement
15 |       const widgetStore = useWidgetStore()
16 |       const target = widgetStore.dActiveElement
17 |       if (!target) return
18 |       // 设置移动状态初始值
19 |       widgetStore.initDMove({
20 |         startX: e.pageX,
21 |         startY: e.pageY,
22 |         originX: target.left,
23 |         originY: target.top,
24 |       })
25 | 
26 |       // 绑定鼠标移动事件
27 |       document.addEventListener('mousemove', this.handlemousemove, true)
28 | 
29 |       // 取消鼠标移动事件
30 |       document.addEventListener('mouseup', this.handlemouseup, true)
31 |     },
32 | 
33 |     handlemousemove(e: MouseEvent) {
34 |       const widgetStore = useWidgetStore()
35 |       e.stopPropagation()
36 |       e.preventDefault()
37 | 
38 |       widgetStore.dMove({
39 |         x: e.pageX,
40 |         y: e.pageY,
41 |       })
42 |     },
43 | 
44 |     handlemouseup() {
45 |       const controlStore = useControlStore()
46 |       document.removeEventListener('mousemove', this.handlemousemove, true)
47 |       document.removeEventListener('mouseup', this.handlemouseup, true)
48 |       controlStore.stopDMove()
49 |     },
50 |   },
51 | }
52 | 
53 | const moveInit = {
54 |   methods: {
55 |     initmovement(e: MouseEvent) {
56 |       const controlStore = useControlStore()
57 |       const widgetStore = useWidgetStore()
58 |       if (!controlStore.dAltDown) {
59 |         // 设置mouseevent给moveable初始
60 |         // 在组合操作时排除
61 |         widgetStore.setMouseEvent(e)
62 |       }
63 | 
64 |       const target = widgetStore.dActiveElement
65 |       if (!target) return
66 |       widgetStore.initDMove({
67 |         startX: e.pageX,
68 |         startY: e.pageY,
69 |         originX: target.left,
70 |         originY: target.top,
71 |       })
72 | 
73 |       const handlemouseup = () => {
74 |         const widgetStore = useWidgetStore()
75 |         // 销毁选中即刻移
76 |         widgetStore.setMouseEvent(null)
77 |         
78 |         document.removeEventListener('mouseup', handlemouseup, true)
79 |       }
80 |       document.addEventListener('mouseup', handlemouseup, true)
81 |     },
82 |   },
83 | }
84 | 
85 | export { move, moveInit }
86 | 


--------------------------------------------------------------------------------
/src/mixins/scKeyCodes.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-04 00:36:13
 4 |  * @Description: 快捷键支持列表
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-11 15:05:41
 7 |  */
 8 | const ctrlKey = isMacOS() ? `⌘` : `Ctrl`
 9 | function isMacOS() {
10 |   return navigator.userAgent.includes(`Macintosh`) || navigator.userAgent.includes(`Mac OS X`)
11 | }
12 | 
13 | export default [
14 |   {
15 |     feat: `拖拽画布`,
16 |     info: `空格 + 鼠标拖拽`,
17 |   },
18 |   {
19 |     feat: `画布缩小`,
20 |     info: `${ctrlKey} - / ${ctrlKey} + 滚轮`,
21 |   },
22 |   {
23 |     feat: `画布放大`,
24 |     info: `${ctrlKey} + / ${ctrlKey} + 滚轮`,
25 |   },
26 |   {
27 |     feat: `保存`,
28 |     info: `${ctrlKey} + S`,
29 |   },
30 |   {
31 |     feat: `撤销`,
32 |     info: `${ctrlKey} + Z`,
33 |   },
34 |   {
35 |     feat: `重做`,
36 |     info: `${ctrlKey} + Shift + Z`,
37 |   },
38 |   {
39 |     feat: `复制`,
40 |     info: `${ctrlKey} + C`,
41 |   },
42 |   {
43 |     feat: `粘贴`,
44 |     info: `${ctrlKey} + V`,
45 |   },
46 |   {
47 |     feat: `删除`,
48 |     info: `Delete / Backspace`,
49 |   },
50 |   {
51 |     feat: `元素移动`,
52 |     info: `← ↑ → ↓`,
53 |   },
54 |   {
55 |     feat: `快速移动`,
56 |     info: `Shift + ← ↑ → ↓`,
57 |   },
58 |   {
59 |     feat: `多选`,
60 |     info: `${ctrlKey} / Shift + 点选`,
61 |   },
62 |   {
63 |     feat: `成组`,
64 |     info: `${ctrlKey} + G`,
65 |   },
66 |   {
67 |     feat: `取消选中`,
68 |     info: `ESC`,
69 |   },
70 | ]
71 | 


--------------------------------------------------------------------------------
/src/router/base.ts:
--------------------------------------------------------------------------------
 1 | import { RouteRecordRaw } from 'vue-router';
 2 | 
 3 | /*
 4 |  * @Author: ShawnPhang
 5 |  * @Date: 2021-08-19 18:43:22
 6 |  * @Description: 前端路由
 7 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 8 |  * @LastEditTime: 2023-09-19 17:32:04
 9 |  */
10 | export default [
11 |   // {
12 |   //   path: '/',
13 |   //   name: 'main',
14 |   //   redirect: 'home',
15 |   //   component: () => import('@/views/Ready.vue'),
16 |   //   children: [
17 |   //     {
18 |   //       name: 'Home',
19 |   //       path: '/home',
20 |   //       component: () => import(/* webpackChunkName: 'base' */ '@/views/Index.vue'),
21 |   //     },
22 |   //   ],
23 |   // },
24 |   {
25 |     // 预留主页
26 |     path: '/',
27 |     name: 'main',
28 |     redirect: 'home',
29 |   },
30 |   {
31 |     path: '/home',
32 |     name: 'Home',
33 |     component: () => import(/* webpackChunkName: 'base' */ '@/views/Index.vue'),
34 |   },
35 |   {
36 |     path: '/draw',
37 |     name: 'Draw',
38 |     component: () => import(/* webpackChunkName: 'draw' */ '@/views/Draw.vue'),
39 |   },
40 |   {
41 |     path: "/html",
42 |     name: "Html",
43 |     component: () => import(/* webpackChunkName: 'html' */ '@/views/Html.vue'),
44 |   },
45 |   {
46 |     path: '/psd',
47 |     name: 'Psd',
48 |     component: () => import(/* webpackChunkName: 'psd' */ '@/views/Psd.vue'),
49 |   },
50 | ] as RouteRecordRaw[]
51 | 


--------------------------------------------------------------------------------
/src/router/hook.ts:
--------------------------------------------------------------------------------
 1 | //  import store from '@/store'
 2 | 
 3 | import { NavigationGuardNext, RouteLocationNormalized, Router } from "vue-router"
 4 | 
 5 |  export default (router: Router) => {
 6 |  
 7 |      router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
 8 |          // if (to.meta.requireAuth) { }
 9 |  
10 |          // 有必要时清除残余的loading框
11 |          // store.commit('loading', false);
12 |  
13 |         //  const $store = store as Type.Object
14 |         //  $store.commit('changeRoute', from.path)
15 |  
16 |          if (/\/http/.test(to.path) || /\/https/.test(to.path)) {
17 |              const url = to.path.split('http')[1]
18 |              window.location.href = `http${url}`
19 |          } else {
20 |              next()
21 |          }
22 |  
23 |      })
24 |  
25 |      router.afterEach(() => {
26 |          window.scrollTo(0, 0);
27 |      })
28 |  }
29 |  
30 |  
31 |  


--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-19 18:43:22
 4 |  * @Description: Router Enter
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-09-15 12:37:11
 7 |  */
 8 | import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw } from 'vue-router'
 9 | import config from '@/config'
10 | import hook from './hook'
11 | import base from './base'
12 | 
13 | const routes: Array<RouteRecordRaw> = [...base]
14 | 
15 | const router = createRouter({
16 |   history: createWebHistory(config.BASE_URL), // import.meta.env.BASE_URL
17 |   // history: createWebHashHistory(),
18 |   routes,
19 | })
20 | 
21 | hook(router)
22 | 
23 | export default router
24 | 


--------------------------------------------------------------------------------
/src/store/base/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-17 15:00:00
 4 |  * @Description: Base全局状态管理
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-08 17:00:12
 7 |  */
 8 | 
 9 | import { Store, defineStore } from 'pinia'
10 | 
11 | // import actions from './actions'
12 | // import _config from '@/config'
13 | 
14 | type TStoreBaseState = {
15 |   loading: boolean | null
16 |   watermark: string | string[]
17 |   /** fonts */
18 |   fonts: string[]
19 | }
20 | 
21 | type TUserAction = {
22 |   hideLoading: () => void
23 |   setFonts: (list: string[]) => void
24 |   changeWatermark: (e: string[] | string) => void
25 | }
26 | 
27 | /** Base全局状态管理 */
28 | const useBaseStore = defineStore<'base', TStoreBaseState, {}, TUserAction>('base', {
29 |   state: () => ({
30 |     loading: null,
31 |     watermark: ['迅排设计', 'poster-design'],
32 |     fonts: [], // 缓存字体列表
33 |   }),
34 |   actions: {
35 |     /** 隐藏loading */
36 |     hideLoading() {
37 |       setTimeout(() => {
38 |         this.loading = false
39 |       }, 600)
40 |     },
41 |     setFonts(list: string[]) {
42 |       this.fonts = list
43 |     },
44 |     changeWatermark(wm: any) {
45 |       this.watermark = wm
46 |     }
47 |   }
48 | })
49 | 
50 | export type TBaseStore = Store<'base', TStoreBaseState, {}, TUserAction>
51 | 
52 | export default useBaseStore
53 | 
54 | 


--------------------------------------------------------------------------------
/src/store/base/user.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-17 15:00:00
 4 |  * @Description: User全局状态管理
 5 |  * @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-18 21:00:00
 7 |  */
 8 | 
 9 | import { Store, defineStore } from "pinia"
10 | 
11 | type TUserStoreState = {
12 |   /** 登录状态 */
13 |   online: boolean
14 |   /** 储存用户信息 */
15 |   user: {
16 |     name: string | null
17 |   }
18 |   /**是否为管理员模式 */
19 |   manager: string
20 |   /** 管理员是否正在编辑模板 */
21 |   tempEditing: boolean
22 | }
23 | 
24 | type TUserAction = {
25 |   /** 修改登录状态 */
26 |   changeOnline: (state: boolean) => void
27 |   /** 修改登录用户 */
28 |   changeUser: (userName: string) => void
29 |   managerEdit: (status: boolean) => void
30 | }
31 | 
32 | /** User全局状态管理 */
33 | const useUserStore = defineStore<'userStore', TUserStoreState, {}, TUserAction>('userStore', {
34 |   state: () => ({
35 |     online: true, // 登录状态,
36 |     user: {
37 |       name: localStorage.getItem('username'),
38 |     }, // 储存用户信息
39 |     manager: '', // 是否为管理员模式
40 |     tempEditing: false, // 管理员是否正在编辑模板
41 |   }),
42 |   actions: {
43 |     changeOnline(status: boolean) {
44 |       this.online = status
45 |     },
46 |     changeUser(name: string) {
47 |       this.user.name = name
48 |       // state.user = Object.assign({}, state.user)
49 |       // state.user = { ...state.user }
50 |       localStorage.setItem('username', name)
51 |     },
52 |     managerEdit(status: boolean) {
53 |       this.tempEditing = status
54 |     },
55 |     
56 |   }
57 | })
58 | 
59 | export type TUserStore = Store<'userStore', TUserStoreState, {}, TUserAction>
60 | 
61 | export default useUserStore
62 | 


--------------------------------------------------------------------------------
/src/store/design/canvas/d.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang <https://m.palxp.cn>
 3 |  * @Date: 2024-04-05 06:23:23
 4 |  * @Description:  
 5 |  * @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-09-25 00:39:00
 7 |  */
 8 | export type TScreeData = {
 9 |   /** 记录编辑界面的宽度 */
10 |   width: number
11 |   /** 记录编辑界面的高度 */
12 |   height: number
13 | }
14 | 
15 | export type TGuidelinesData = {
16 |   verticalGuidelines: number[]
17 |   horizontalGuidelines: number[]
18 | }
19 | 
20 | export type TCanvasState = {
21 |   /** 画布缩放百分比 */
22 |   dZoom: number
23 |   /** 画布默认预留边距 */
24 |   dPresetPadding: number,
25 |   /** 画布底部工具栏高度 */
26 |   dBottomHeight: number,
27 |   /** 画布垂直居中修正值 */
28 |   dPaddingTop: number
29 |   /** 编辑界面 */
30 |   dScreen: TScreeData
31 |   /** 标尺辅助线 */
32 |   guidelines: TGuidelinesData
33 |   /** 页面数据 */
34 |   dPage: TPageState
35 |   /** 当前页面下标 */
36 |   dCurrentPage: number
37 | }
38 | 
39 | export type TStoreAction = {
40 |   /** 更新画布缩放百分比 */
41 |   updateZoom: (zoom: number) => void
42 |   /** 更新画布垂直居中修正值 */
43 |   updatePaddingTop: (num: number) => void
44 |   /** 更新编辑界面的宽高 */
45 |   updateScreen: (data: TScreeData) => void
46 |   /** 修改标尺线 */
47 |   updateGuidelines: (lines: TGuidelinesData) => void
48 |   /** 强制重绘画布 */
49 |   reChangeCanvas: () => void
50 |   /** 更新Page数据 */
51 |   updatePageData<T extends keyof TPageState>(data: {
52 |     key: T
53 |     value: TPageState[T]
54 |     // pushHistory?: boolean
55 |   }): void
56 |   getDPage(data: TPageState): TPageState
57 |   /** 设置dPage */
58 |   setDPage(data: TPageState): void
59 |   /** 更新 Page(从layouts获取)*/
60 |   updateDPage(): void
61 |   /** 设置底部工具栏高度 */
62 |   setBottomHeight(h: number): void
63 |   /** 更新当前页面下标 */
64 |   setDCurrentPage(n: number): void
65 | }
66 | 
67 | export type TPageState = {
68 |   name: string
69 |   type: string
70 |   uuid: string
71 |   left: number
72 |   top: number
73 |   /** 画布宽度 */
74 |   width: number
75 |   /** 画布高度 */
76 |   height: number
77 |   /** 画布背景颜色 */
78 |   backgroundColor: string
79 |   /** 画布背景颜色(兼容渐变色) */
80 |   backgroundGradient: string,
81 |   /** 画布背景图片 */
82 |   backgroundImage: string
83 |   backgroundTransform: {
84 |     x?: number
85 |     y?: number
86 |   }
87 |   /** 透明度 */
88 |   opacity: number
89 |   /** 强制刷新用 */
90 |   tag: number
91 | }
92 | 
93 | 


--------------------------------------------------------------------------------
/src/store/design/canvas/page-default.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-16 10:06:23
 4 |  * @Description:  
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-19 15:42:40
 7 |  */
 8 | export default {
 9 |   name: '新画板',
10 |   type: 'page',
11 |   uuid: '-1',
12 |   left: 0,
13 |   top: 0,
14 |   width: 1920, // 画布宽度
15 |   height: 1080, // 画布高度
16 |   backgroundColor: '#ffffffff', // 画布背景颜色
17 |   backgroundGradient: '', // 用于兼容渐变颜色
18 |   backgroundImage: '', // 画布背景图片
19 |   backgroundTransform: {},
20 |   opacity: 1, // 透明度
21 |   tag: 0, // 强制刷新用
22 |   record: {}
23 | }
24 | 


--------------------------------------------------------------------------------
/src/store/design/force/index.ts:
--------------------------------------------------------------------------------
 1 | import { Store, defineStore } from "pinia";
 2 | 
 3 | 
 4 | 
 5 | type TForceState = {
 6 |   /** 画布强制刷新适应度 */
 7 |   zoomScreenChange: number | null
 8 |   /** 强制刷新操作框 */
 9 |   updateRect: number | null
10 |   /** 强制设置选择元素 */
11 |   updateSelect: number | null
12 | }
13 | 
14 | type TForceAction = {
15 |   setZoomScreenChange: () => void
16 |   setUpdateRect: () => void
17 |   setUpdateSelect: () => void
18 | }
19 | 
20 | const ForceStore = defineStore<"forceStore", TForceState, {}, TForceAction>("forceStore", {
21 |   state: () => ({
22 |     zoomScreenChange: null, // 画布强制刷新适应度
23 |     updateRect: null, // 强制刷新操作框
24 |     updateSelect: null, // 强制设置选择元素
25 |   }),
26 | 
27 |   actions: {
28 |     setZoomScreenChange() {
29 |       // 画布尺寸适应度强制刷新
30 |       this.zoomScreenChange = Math.random()
31 |     },
32 |     setUpdateRect() {
33 |       // 强制刷新操作框
34 |       this.updateRect = Math.random()
35 |     },
36 |     setUpdateSelect() {
37 |       // 强制触发元素选择
38 |       this.updateSelect = Math.random()
39 |     },
40 |   }
41 | })
42 | 
43 | export type TForceStore = Store<"forceStore", TForceState, {}, TForceAction>
44 | 
45 | export default ForceStore
46 | 


--------------------------------------------------------------------------------
/src/store/design/group/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-18 21:00:00
 4 |  * @Description: Store方法export
 5 |  * @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-28 14:00:00
 7 |  */
 8 | 
 9 | import { Store, defineStore } from "pinia";
10 | import { getCombined, initGroupJson, realCombined } from "./action";
11 | import { TdWidgetData } from "../widget";
12 | 
13 | type TGroupState = {
14 |   /** 组合的json数据 */
15 |   dGroupJson: string
16 | }
17 | 
18 | type TGroupAction = {
19 |   realCombined(): void
20 |   getCombined(): Promise<TdWidgetData>
21 |   initGroupJson(data: string): void
22 | }
23 | 
24 | const GroupStore = defineStore<"groupStore", TGroupState, {}, TGroupAction>("groupStore", {
25 |   state: () => ({
26 |     dGroupJson: "" // 组合的json数据
27 |   }),
28 | 
29 |   actions: {
30 |     realCombined() { realCombined(this) },
31 |     getCombined() { return getCombined(this) },
32 |     initGroupJson(data) { initGroupJson(this, data) },
33 |   }
34 | })
35 | 
36 | export type TGroupStore = Store<"groupStore", TGroupState, {}, TGroupAction>
37 | 
38 | export default GroupStore
39 | 


--------------------------------------------------------------------------------
/src/store/design/history/actions/pushHistory.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-18 21:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 09:29:10
 7 |  */
 8 | 
 9 | import { useCanvasStore, useWidgetStore } from "@/store"
10 | import { THistoryStore } from ".."
11 | 
12 | /** push操作历史记录(历史记录功能已重构,该方法不再使用) */
13 | export function pushHistory(store: THistoryStore, msg: string = "") {
14 |   // const pageStore = useCanvasStore()
15 |   // const widgetStore = useWidgetStore()
16 |   // console.log('history压栈', '来源: ' + msg)
17 |   // // 如果有上一次记录,并且与新纪录相同,那么不继续操作
18 |   // if (store.dHistory[store.dHistory.length - 1] && store.dHistory[store.dHistory.length - 1] === JSON.stringify(widgetStore.dWidgets)) {
19 |   //   return
20 |   // }
21 |   // if (store.dHistoryParams.index < history.length - 1) {
22 |   //   const index = store.dHistoryParams.index + 1
23 |   //   const len = history.length - index
24 |   //   store.dHistory.splice(index, len)
25 |   //   store.dPageHistory.splice(index, len)
26 |   //   store.dHistoryParams.length = store.dHistory.length
27 |   //   store.dHistoryParams.index = store.dHistory.length - 1
28 |   // }
29 |   // store.dHistory.push(JSON.stringify(widgetStore.dWidgets))
30 |   // store.dPageHistory.push(JSON.stringify(pageStore.dPage))
31 |   // store.dHistoryParams.index = store.dHistory.length - 1
32 |   // store.dHistoryParams.length = store.dHistory.length
33 | }
34 | 
35 | /** 添加颜色选择历史记录 */
36 | export function pushColorToHistory(store: THistoryStore, color: string) {
37 |   const history = store.dColorHistory
38 |   // 如果已经存在就提到前面来,避免重复
39 |   const index = history.indexOf(color)
40 |   if (index !== -1) {
41 |     history.splice(index, 1)
42 |   }
43 |   // 最多保存3种颜色
44 |   if (history.length === 4) {
45 |     history.splice(history.length - 1, 1)
46 |   }
47 |   // 把最新的颜色放在头部
48 |   const head = [color]
49 |   store.dColorHistory = head.concat(history)
50 | }
51 | 


--------------------------------------------------------------------------------
/src/store/design/history/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-18 21:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 09:28:41
 7 |  */
 8 | 
 9 | import { Store, defineStore } from 'pinia'
10 | import { pushColorToHistory } from './actions/pushHistory'
11 | import handleHistory from './actions/handleHistory'
12 | import { useCanvasStore, useWidgetStore } from '@/store'
13 | 
14 | export type THistoryParamData = {
15 |   index: number
16 |   length: number
17 |   maxLength: number
18 |   stackPointer: number
19 | }
20 | export type THsitoryStack = {
21 |   changes: any[]
22 |   inverseChanges: any[]
23 | }
24 | 
25 | type THistoryState = {
26 |   /** 记录历史操作(保存整个画布的json数据) */
27 |   dHistory: string[]
28 |   /** 记录历史操作对应的page */
29 |   dPageHistory: string[]
30 |   /** 记录差分补丁 */
31 |   dHistoryStack: THsitoryStack
32 |   /** 记录指针等数据 */
33 |   dHistoryParams: THistoryParamData
34 |   /** 记录历史选择的颜色 */
35 |   dColorHistory: string[]
36 | }
37 | 
38 | type THistoryAction = {
39 |   /** 写入历史记录 */
40 |   changeHistory: (patches: any) => void
41 |   /**
42 |    * 操作历史记录
43 |    * action为undo表示撤销
44 |    * action为redo表示重做
45 |    */
46 |   handleHistory: (action: 'undo' | 'redo') => void
47 |   pushColorToHistory: (color: string) => void
48 | }
49 | 
50 | /** 历史记录Store */
51 | const HistoryStore = defineStore<'historyStore', THistoryState, {}, THistoryAction>('historyStore', {
52 |   state: () => ({
53 |     dHistory: [],
54 |     dHistoryParams: {
55 |       index: -1,
56 |       length: 0,
57 |       maxLength: 20,
58 |       stackPointer: -1
59 |     },
60 |     dHistoryStack: {
61 |       changes: [],
62 |       inverseChanges: [],
63 |     },
64 |     dColorHistory: [],
65 |     dPageHistory: [],
66 |   }),
67 | 
68 |   actions: {
69 |     changeHistory({ patches, inversePatches }) {
70 |       const pointer = ++this.dHistoryParams.stackPointer
71 |       // 如若之前撤销过,当新增记录时,后面的记录就清空了
72 |       this.dHistoryStack.changes.length = pointer
73 |       this.dHistoryStack.inverseChanges.length = pointer
74 |       this.dHistoryStack.changes[pointer] = patches
75 |       this.dHistoryStack.inverseChanges[pointer] = inversePatches
76 |     },
77 |     handleHistory(action) {
78 |       handleHistory(this, action)
79 |       // TODO: 操作后如果当前选中元素还在,则应当保留选择框
80 |       // widgetStore.setdActiveElement(pageStore.dPage)
81 |     },
82 |     pushColorToHistory(color) {
83 |       pushColorToHistory(this, color)
84 |     },
85 |   },
86 | })
87 | 
88 | export type THistoryStore = Store<'historyStore', THistoryState, {}, THistoryAction>
89 | 
90 | export default HistoryStore
91 | 


--------------------------------------------------------------------------------
/src/store/design/widget/actions/align.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-28 14:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 09:29:27
 7 |  */
 8 | 
 9 | import { useCanvasStore, useHistoryStore } from "@/store"
10 | import { TWidgetStore, TdWidgetData } from ".."
11 | 
12 | type TAlign = 'left' | 'ch' | 'right' | 'top' | 'cv' | 'bottom'
13 | 
14 | export type TUpdateAlignData = {
15 |   align: TAlign
16 |   uuid: string
17 |   group?: TdWidgetData
18 | }
19 | 
20 | export function updateAlign(store: TWidgetStore, { align, uuid, group }: TUpdateAlignData) {
21 |   const pageStore = useCanvasStore()
22 |   const historyStore = useHistoryStore()
23 |   const canvasStore = useCanvasStore()
24 | 
25 |   const widgets = store.dWidgets
26 |   const target = uuid ? widgets.find((item: any) => item.uuid === uuid) : store.dActiveElement
27 |   let parent = group || pageStore.dPage
28 | 
29 |   if (!target) return
30 | 
31 |   if (target.parent !== '-1') {
32 |     const tmp = widgets.find((item: any) => item.uuid === target.parent)
33 |     tmp && (parent = tmp)
34 |   }
35 | 
36 |   let left = target.left
37 |   let top = target.top
38 |   let pw = parent.record.width || parent.width
39 |   let ph = parent.record.height || parent.height
40 | 
41 |   if (parent.uuid === '-1') {
42 |     pw = parent.width
43 |     ph = parent.height
44 |   }
45 | 
46 |   const targetW = target.width
47 |   const targetH = target.height
48 |   switch (align) {
49 |     case 'left':
50 |       left = parent.left
51 |       break
52 |     case 'ch': // 水平居中
53 |       left = parent.left + pw / 2 - targetW / 2
54 |       break
55 |     case 'right':
56 |       left = parent.left + pw - targetW
57 |       break
58 |     case 'top':
59 |       top = parent.top
60 |       break
61 |     case 'cv': // 垂直居中
62 |       top = parent.top + ph / 2 - targetH / 2
63 |       break
64 |     case 'bottom':
65 |       top = parent.top + ph - targetH
66 |       break
67 |   }
68 | 
69 |   if (target.left !== left || target.top !== top) {
70 |     if (target.isContainer) {
71 |       const dLeft = target.left - left
72 |       const dTop = target.top - top
73 |       const len = widgets.length
74 |       for (let i = 0; i < len; ++i) {
75 |         const widget = widgets[i]
76 |         if (widget.parent === target.uuid) {
77 |           widget.left -= dLeft
78 |           widget.top -= dTop
79 |         }
80 |       }
81 |     }
82 |     target.left = left
83 |     target.top = top
84 | 
85 |     canvasStore.reChangeCanvas()
86 |     // store.dispatch('reChangeCanvas')
87 |   }
88 | }
89 | 


--------------------------------------------------------------------------------
/src/store/design/widget/actions/group.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-28 21:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 09:29:31
 7 |  */
 8 | 
 9 | import { useCanvasStore, useHistoryStore } from "@/store"
10 | import { TWidgetStore, TdWidgetData } from ".."
11 | import { customAlphabet } from 'nanoid/non-secure'
12 | const nanoid = customAlphabet('1234567890abcdef', 12)
13 | 
14 | 
15 | export function addGroup(store: TWidgetStore, group: TdWidgetData[]) {
16 |   const historyStore = useHistoryStore()
17 |   const canvasStore = useCanvasStore()
18 |   let parent: TdWidgetData | null = null
19 |   group.forEach((item) => {
20 |     item.uuid = nanoid() // 重设id
21 |     item.type === 'w-group' && (parent = item) // 找出父组件
22 |   })
23 |   group.forEach((item) => {
24 |     !item.isContainer && parent && (item.parent = parent.uuid) // 重设父id
25 |     item.text && (item.text = decodeURIComponent(item.text))
26 |     store.dWidgets.push(item)
27 |   })
28 |   // 选中组件
29 |   const len = store.dWidgets.length
30 |   store.dActiveElement = store.dWidgets[len - 1]
31 | 
32 |   canvasStore.reChangeCanvas()
33 |   // store.dispatch('reChangeCanvas')
34 | }


--------------------------------------------------------------------------------
/src/store/design/widget/actions/template.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-28 21:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-12 09:29:35
 7 |  */
 8 | 
 9 | 
10 | import { customAlphabet } from 'nanoid/non-secure'
11 | import { TWidgetStore, TdWidgetData } from '..'
12 | import { useCanvasStore, useWidgetStore } from '@/store'
13 | const nanoid = customAlphabet('1234567890abcdef', 12)
14 | 
15 | // TODO: 选择模板
16 | export function setTemplate(store: TWidgetStore, allWidgets: TdWidgetData[]) {
17 |   // const historyStore = useHistoryStore()
18 |   const canvasStore = useCanvasStore()
19 |   const widgetStore = useWidgetStore()
20 |   allWidgets.forEach((item) => {
21 |     Number(item.uuid) < 0 && (item.uuid = nanoid()) // 重设id
22 |     item.text && (item.text = decodeURIComponent(item.text))
23 |     store.dWidgets.push(item)
24 |   })
25 |   widgetStore.updateDWidgets()
26 |   canvasStore.reChangeCanvas()
27 | }
28 | 


--------------------------------------------------------------------------------
/src/store/design/widget/getter/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-28 14:00:00
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-18 15:14:48
 7 |  */
 8 | 
 9 | import { useCanvasStore } from '@/store'
10 | import { TWidgetState, TdWidgetData } from '..'
11 | import { TPageState } from '@/store/design/canvas/d'
12 | 
13 | export type TWidgetJsonData = TPageState & {
14 |   widgets: TdWidgetData
15 | }
16 | 
17 | /** 返回组件Json数据 */
18 | export function widgetJsonData(state: TWidgetState) {
19 |   const pageStore = useCanvasStore()
20 |   // const page: TWidgetJsonData = JSON.parse(JSON.stringify(pageStore.dPage))
21 |   !state.dLayouts[pageStore.dCurrentPage] && pageStore.dCurrentPage--
22 |   return state.dLayouts[pageStore.dCurrentPage].layers
23 | }
24 | 


--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: Jeremy Yu
 3 |  * @Date: 2024-03-18 21:00:00
 4 |  * @Description: Store方法export
 5 |  * @LastEditors: Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-18 21:00:00
 7 |  */
 8 | 
 9 | import useBaseStore from "./base";
10 | import useUserStore from "./base/user";
11 | import useCanvasStore from "./design/canvas"
12 | import useControlStore from './design/control'
13 | import useHistoryStore from './design/history'
14 | import useWidgetStore from './design/widget'
15 | import useGroupStore from './design/group'
16 | import useForceStore from './design/force'
17 | 
18 | export {
19 |   useBaseStore,
20 |   useUserStore,
21 |   useCanvasStore,
22 |   useControlStore,
23 |   useHistoryStore,
24 |   useWidgetStore,
25 |   useGroupStore,
26 |   useForceStore,
27 | }
28 | 


--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | /** 公共API返回结果 */
 4 | type TCommResResult<T> = {
 5 |   code: number
 6 |   msg: string
 7 |   result: T
 8 | }
 9 | 
10 | type TCommonItemData = {
11 |   type: string
12 |   fontFamily?: string
13 |   color?: string
14 |   fontSize: number
15 |   width: number
16 |   height: number
17 |   left: number
18 |   top: number
19 |   fontWeight: number
20 |   value: TItem2DataParam
21 | }
22 | 
23 | /** 分页查询公共返回 */
24 | type TPageRequestResult<T> = {
25 |   list: T
26 |   total: number
27 | }
28 | 
29 | interface HTMLElementEventMap {
30 |   "mousewheel": MouseEvent
31 | }
32 | 
33 | interface IQiniuSubscribeCb {
34 |   (result: {
35 |     total: { percent: number }
36 |     key: string
37 |     hash: string
38 |   }): void
39 | }
40 | 
41 | interface Window {
42 |   qiniu: {
43 |     upload: (
44 |       file: File | Blob,
45 |       name: string,
46 |       token: string,
47 |       exObj: Record<string, any>,
48 |       exOption: {
49 |         useCdnDomain: boolean
50 |       }) => {
51 |         subscribe: (cb: {
52 |           next: IQiniuSubscribeCb
53 |           error: (err: string) => void
54 |           complete: IQiniuSubscribeCb
55 |         }) => void
56 |       }
57 |   }
58 | }
59 | 
60 | 
61 | interface MouseEvent {
62 |   layerX: number
63 |   layerY: number
64 | }
65 | 
66 | interface Document {
67 |   selection?: Selection
68 | }
69 | 
70 | interface HTMLElement {
71 |   createTextRange(): {
72 |     moveToElementText(el: HTMLElement): void
73 |     select(): void
74 |   }
75 | }
76 | 
77 | 


--------------------------------------------------------------------------------
/src/types/properties.d.ts:
--------------------------------------------------------------------------------
 1 | import { AxiosInstance } from 'axios'
 2 | 
 3 | interface myAxios {
 4 |   [propName: string]: any
 5 | }
 6 | 
 7 | declare module '@vue/runtime-core' {
 8 |   interface ComponentCustomProperties {
 9 |     $ajax: myAxios
10 |     $utils: Type.Object
11 |   }
12 | }
13 | 


--------------------------------------------------------------------------------
/src/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module '*.vue' {
3 |   import type { DefineComponent } from 'vue'
4 |   const component: DefineComponent<{}, {}, any>
5 |   export default component
6 | }
7 | 


--------------------------------------------------------------------------------
/src/types/style.d.ts:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | namespace StyleProperty {
4 |   type TextAlign = "center" | "end" | "left" | "right" | "start" | "justify";
5 |   type WritingMode = Globals | "horizontal-tb" | "sideways-lr" | "sideways-rl" | "vertical-lr" | "vertical-rl";
6 | }


--------------------------------------------------------------------------------
/src/types/vue-ts.d.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-07-11 14:21:33
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-17 15:56:39
 7 |  */
 8 | import Vue, { VNode } from 'vue'
 9 | 
10 | declare global {
11 |   namespace Type {
12 |     export interface Object {
13 |       [propName: string]: any
14 |     }
15 |   }
16 |   namespace Ajax {
17 |     // reqposne interface
18 |     export interface GqlResult {
19 |       [field: string]: any
20 |     }
21 | 
22 |     // axios return data
23 |     export interface Gql {
24 |       [field: string]: GqlResult
25 |     }
26 | 
27 |     export interface Result {
28 |       [field: string]: any
29 |     }
30 |   }
31 | }
32 | 
33 | declare module '*.vue' {
34 |   export default Vue
35 | }
36 | 
37 | declare module '~data'
38 | declare module 'qrcode'
39 | declare module '@antv/f2/*'
40 | declare module 'dayjs'
41 | declare module 'fontfaceobserver'
42 | declare module 'throttle-debounce'
43 | declare module 'html2canvas'
44 | declare module 'psd.js'
45 | 
46 | declare module 'vue/types/vue' {
47 |   interface Vue {
48 |     $utils: Type.Object
49 |     $nextTick: any
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------
/src/types/vuex-shim.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentCustomProperties } from 'vue'
2 | 
3 | declare module '@vue/runtime-core' {
4 |   // Declare your own store states.
5 |   interface State {
6 |     count: number
7 |   }
8 | }
9 | 


--------------------------------------------------------------------------------
/src/types/worker.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 |  * @Author: ShawnPhang
3 |  * @Date: 2023-09-14 14:40:06
4 |  * @Description:
5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
6 |  * @LastEditTime: 2023-09-14 14:40:13
7 |  */
8 | declare function importScripts(...urls: string[]): void
9 | 


--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-07-13 02:48:38
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-13 18:19:28
 7 |  */
 8 | import * as services from '../api/index'
 9 | import * as utils from './utils'
10 | import _config from '@/config'
11 | import modules from './plugins/modules'
12 | import cssLoader from './plugins/cssLoader'
13 | import type { App } from 'vue'
14 | 
15 | /**
16 |  * 全局组件方法
17 |  */
18 | export default {
19 |   install(myVue: App) {
20 |     /** 全局组件注册 */
21 |     modules(myVue)
22 |     /** iconfont 注入 */
23 |     cssLoader(_config.ICONFONT_EXTRA)
24 |     cssLoader(_config.ICONFONT_URL)
25 | 
26 |     myVue.config.globalProperties.$ajax = services
27 | 
28 |     myVue.config.globalProperties.$utils = utils
29 | 
30 |     // baidu statistics
31 |     ;(function () {
32 |       const hm = document.createElement('script')
33 |       hm.src = 'https://hm.baidu.com/hm.js?21238d2872af8b12083429237026b84c'
34 |       const s: any = document.getElementsByTagName('script')[0]
35 |       s.parentNode.insertBefore(hm, s)
36 |     })()
37 |   },
38 | }
39 | 


--------------------------------------------------------------------------------
/src/utils/plugins/cssLoader.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-02 18:13:32
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang
 6 |  * @LastEditTime: 2021-08-02 18:13:52
 7 |  */
 8 | 
 9 | export default (url: string) => {
10 |   const link_element = document.createElement('link')
11 |   link_element.setAttribute('rel', 'stylesheet')
12 |   link_element.setAttribute('href', url)
13 |   document.head.appendChild(link_element)
14 | }
15 | 


--------------------------------------------------------------------------------
/src/utils/plugins/eventBus.ts:
--------------------------------------------------------------------------------
1 | import mitt from 'mitt';
2 | 
3 | type Events = {
4 |   refreshUserImages: any;
5 | };
6 | 
7 | const emitter = mitt<Events>();
8 | 
9 | export default emitter;


--------------------------------------------------------------------------------
/src/utils/plugins/modules.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-07-14 11:43:13
 4 |  * @Description: 全局组件导入
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-08 18:23:15
 7 |  */
 8 | import coms from '@/components/modules'
 9 | import pageStyle from '@/components/modules/layout/designBoard/pageStyle.vue'
10 | import { App } from 'vue'
11 | 
12 | export default (Vue: App) => {
13 |   coms(Vue)
14 |   Vue.component('page-style', pageStyle) // 背景属性已不在 modules/widgets 中,单独注册
15 |   // Vue.use(Field).use(Divider).use(NavBar).use(Toast).use(Popup)
16 | }
17 | 


--------------------------------------------------------------------------------
/src/utils/plugins/pointImg.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-06 13:53:30
 4 |  * @Description: 获取图片在鼠标焦点的颜色
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2023-09-19 17:32:40
 7 |  */
 8 | export default class PointImg {
 9 |   private canvas: HTMLCanvasElement | undefined
10 |   private cvs: CanvasRenderingContext2D | null | undefined
11 | 
12 |   constructor(img: HTMLImageElement) {
13 |     if (img.src) {
14 |       try {
15 |         this.canvas = document.createElement('canvas')
16 |         this.canvas.width = img.width
17 |         this.canvas.height = img.height
18 |         img.crossOrigin = 'Anonymous'
19 |         this.cvs = this.canvas.getContext('2d')
20 |         if (!this.cvs) return
21 | 
22 |         this.cvs.drawImage(img, 0, 0, img.width, img.height)
23 |       } catch (error) {
24 |         console.log(error)
25 |       }
26 |     }
27 |   }
28 |   public getColorXY(x: number, y: number) {
29 |     /**
30 |      * @param x Number x坐标起点
31 |      * @param y Number y坐标起点
32 |      * @return color Object 包含颜色的rgba #16进制颜色
33 |      */
34 |     const color: Record<string, string> = {}
35 |     try {
36 |       if (this.cvs) {
37 |         const obj = this.cvs.getImageData(x, y, 1, 1)
38 |         const arr = obj.data.toString().split(',')
39 | 
40 |         let first = parseInt(arr[0], 10).toString(16)
41 |         first = first.length === 2 ? first : first + first
42 | 
43 |         let second = parseInt(arr[1], 10).toString(16)
44 |         second = second.length === 2 ? second : second + second
45 | 
46 |         let third = parseInt(arr[2], 10).toString(16)
47 |         third = third.length === 2 ? third : third + third
48 | 
49 |         let last = parseInt(arr.pop() || '0', 10) / 255
50 |         last = Number(last.toFixed(0))
51 | 
52 |         color['rgba'] = 'rgba(' + arr.join(',') + ',' + last + ')'
53 |         color['#'] = '#' + first + second + third
54 |       }
55 |     } catch (error) {
56 |       // console.log('此为解析图片点位异常')
57 |     }
58 | 
59 |     return color
60 |   }
61 | }
62 | 


--------------------------------------------------------------------------------
/src/utils/plugins/preload.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-12-24 15:13:58
 4 |  * @Description: 资源加载
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>, Jeremy Yu <https://github.com/JeremyYu-cn>
 6 |  * @LastEditTime: 2024-03-05 12:00:00
 7 |  */
 8 | export default class PreLoad {
 9 |   private i: number
10 |   private arr: (string | HTMLImageElement | ChildNode[])[]
11 |   constructor(arr: (string | HTMLImageElement | ChildNode[])[]) {
12 |     this.i = 0
13 |     this.arr = arr
14 |   }
15 |   public imgs() {
16 |     return new Promise<void>((resolve) => {
17 |       const work = (src: string) => {
18 |         if (this.i < this.arr.length) {
19 |           const img = new Image()
20 |           img.src = src
21 |           if (img.complete) {
22 |             work(this.arr[this.i++] as string)
23 |           } else {
24 |             img.onload = () => {
25 |               work(this.arr[this.i++] as string)
26 |               img.onload = null
27 |             }
28 |           }
29 |           // console.log(((this.i + 1) / this.arr.length) * 100);
30 |         } else {
31 |           resolve()
32 |         }
33 |       }
34 |       work(this.arr[this.i] as string)
35 |     })
36 |   }
37 |   public doms() {
38 |     return new Promise<void>((resolve) => {
39 |       const work = () => {
40 |         if (this.i < this.arr.length) {
41 |           (this.arr[this.i] as HTMLImageElement).complete && this.i++
42 |           setTimeout(() => {
43 |             work()
44 |           }, 100)
45 |         } else {
46 |           resolve()
47 |         }
48 |       }
49 |       work()
50 |     })
51 |   }
52 |   /** 判断是否加载svg */
53 |   public svgs() {
54 |     return new Promise<void>((resolve) => {
55 |       const work = () => {
56 |         if (this.i < this.arr.length) {
57 |           (this.arr[this.i] as ChildNode[]).length > 0 && this.i++
58 |           setTimeout(() => {
59 |             work()
60 |           }, 100)
61 |         } else {
62 |           resolve()
63 |         }
64 |       }
65 |       work()
66 |     })
67 |   }
68 | }
69 | 


--------------------------------------------------------------------------------
/src/utils/plugins/psd/color/index.ts:
--------------------------------------------------------------------------------
1 | export * as colorer from './color';


--------------------------------------------------------------------------------
/src/utils/plugins/psd/helper.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-08-17 18:45:24
 4 |  * @Description:
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-18 20:49:49
 7 |  */
 8 | export function createBase64(src: any, { width, height }: any) {
 9 |   let result = ''
10 |   if (src && width) {
11 |     const canvas = document.createElement('canvas')
12 |     canvas.width = width
13 |     canvas.height = height
14 |     const context: any = canvas.getContext('2d', { willReadFrequently: true })
15 |     const imageData = context.getImageData(0, 0, width, height)
16 |     const pixelData = imageData.data
17 |     const len = src.length
18 |     for (let i = 0; i < len; i++) {
19 |       pixelData[i] = src[i]
20 |     }
21 |     context.putImageData(imageData, 0, 0)
22 |     result = canvas.toDataURL('image/png')
23 |   }
24 |   return result
25 | }
26 | 


--------------------------------------------------------------------------------
/src/utils/plugins/webWorker.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2022-03-06 13:53:30
 4 |  * @Description: 计算密集型任务
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-20 16:12:54
 7 |  */
 8 | export default class WebWorker {
 9 |   private worker: Worker | undefined
10 | 
11 |   constructor(useWorker: any) {
12 |     if (typeof Worker === 'undefined') {
13 |       console.error('Web Worker is not supported in this browser.')
14 |     } else {
15 |       // 动态引入无法打包,必须是静态的
16 |       // const file = name ? `../widgets/${name}.worker.ts` : null
17 |       // file &&
18 |       //   (this.worker = new Worker(new URL(file, import.meta.url), {
19 |       //     type: 'module',
20 |       //   }))
21 |       this.worker = new useWorker()
22 |     }
23 |   }
24 |   public start(data?: any, cb?: Function) {
25 |     return new Promise((resolve) => {
26 |       if (!this.worker) resolve('')
27 |       else {
28 |         // 监听Web Worker的消息
29 |         this.worker.onmessage = (e) => {
30 |           cb ? cb(e.data) : resolve(e.data)
31 |         }
32 |         // 发送数据给Web Worker
33 |         this.worker.postMessage(data)
34 |       }
35 |     })
36 |   }
37 |   public send(data?: any) {
38 |     this.worker?.postMessage(data)
39 |   }
40 | }
41 | 


--------------------------------------------------------------------------------
/src/utils/plugins/worker/loadPSD.worker.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-09-14 11:33:44
 4 |  * @Description: 加载PSD解析
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-08-17 18:57:15
 7 |  */
 8 | // import Psd from '@webtoon/psd'
 9 | 
10 | // onmessage = async (e) => {
11 | //   const result = await e.data.arrayBuffer()
12 | //   const rawPsdFile = Psd.parse(result)
13 | //   console.log(111, rawPsdFile)
14 | 
15 | //   const { width, height } = rawPsdFile
16 | //   const psdFile = { width, height }
17 | 
18 | //   const compositeBuffer = await rawPsdFile.composite()
19 | 
20 | //   self.postMessage({ psdFile, compositeBuffer })
21 | // }
22 | 
23 | import { processPSD2Page } from '@/utils/plugins/psd'
24 | 
25 | onmessage = async (e) => {
26 |   const data = await processPSD2Page(e.data)
27 |   self.postMessage({ data })
28 | }


--------------------------------------------------------------------------------
/src/utils/utils.ts:
--------------------------------------------------------------------------------
 1 | import app_config from '@/config'
 2 | export const config = app_config
 3 | 
 4 | type TComObj = Record<string,any>
 5 | 
 6 | // 判断是否在数组中并返回下标
 7 | export const isInArray = (arr: (string | number)[], value: (string | number)) => {
 8 |   const index = arr.indexOf(value)
 9 |   if (index >= 0) {
10 |     return index
11 |   }
12 |   return false
13 | }
14 | 
15 | /** 删除多个对象元素 */
16 | export const deleteSome = <R extends TComObj, T extends TComObj = TComObj>(obj: T, arr: string[]) => {
17 |   arr.forEach((key) => {
18 |     delete obj[key]
19 |   })
20 |   return obj as R extends T ? R : Partial<T>
21 | }
22 | 
23 | /** 拾取对象元素 */
24 | export const pickSome = <R extends TComObj, T extends TComObj = TComObj>(obj: T, arr: string[]) => {
25 |   const newObj: Record<string, any> = {}
26 |   arr.forEach((key) => {
27 |     newObj[key] = obj[key]
28 |   })
29 |   return newObj as R extends T ? R : Partial<T>
30 | }
31 | 
32 | /** 随机数 */
33 | export const rndNum = (n: number, m: number) => {
34 |   const random = Math.floor(Math.random() * (m - n + 1) + n)
35 |   return random
36 | }
37 | 
38 | /** 计算差值 */
39 | export const findClosestNumber = (target: number, numbers: number[]) => {
40 |   if (!Array.isArray(numbers) || numbers.length === 0) {
41 |     throw new Error('数组不能为空')
42 |   }
43 |   let closestNumber = numbers[0]
44 |   let minDifference = Math.abs(target - closestNumber)
45 |   for (let i = 1; i < numbers.length; i++) {
46 |     const currentNumber = numbers[i]
47 |     const currentDifference = Math.abs(target - currentNumber)
48 |     if (currentDifference < minDifference) {
49 |       closestNumber = currentNumber
50 |       minDifference = currentDifference
51 |     }
52 |   }
53 |   return closestNumber
54 | }
55 | 
56 | export default {}
57 | 


--------------------------------------------------------------------------------
/src/utils/widgets/diffLayouts.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-09-14 11:33:44
 4 |  * @Description: 依赖不能直接引入,所以暂时不使用WebWorker
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-19 03:07:18
 7 |  */
 8 | import diff from 'microdiff'
 9 | import { produce, applyPatches, enablePatches } from 'immer'
10 | enablePatches()
11 | const ops: any = {
12 |   CHANGE: 'replace',
13 |   CREATE: 'add',
14 |   REMOVE: 'remove',
15 | }
16 | let cloneData = ''
17 | 
18 | export default class {
19 |   private notifi: any
20 |   constructor() { }
21 |   /**
22 |    * onmessage
23 |    */
24 |   public onmessage(cb: any) {
25 |     this.notifi = cb
26 |   }
27 |   public postMessage(e: any) {
28 |     if (!e) return
29 |     if (e.op === 'done') {
30 |       if (!cloneData) return
31 |       let fork = JSON.parse(cloneData)
32 |       let curArray = JSON.parse(e.data)
33 |       // 比较数据差异
34 |       let diffData: any = diff(fork, curArray)
35 |       // 生成差分补丁
36 |       fork = produce(
37 |         fork,
38 |         (draft) => {
39 |           for (const d of diffData) {
40 |             d.op = ops[d.type]
41 |           }
42 |           draft = applyPatches(draft, diffData)
43 |         },
44 |         (patches, inversePatches) => {
45 |           this.notifi({ patches, inversePatches })
46 |         },
47 |       )
48 |       cloneData = ''
49 |     } else cloneData = e.data
50 |   }
51 | }
52 | 


--------------------------------------------------------------------------------
/src/utils/widgets/history.worker.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-09-14 11:33:44
 4 |  * @Description: 历史记录处理(无效
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-18 20:06:51
 7 |  */
 8 | import diff from 'microdiff'
 9 | import { produce, applyPatches, enablePatches } from 'immer'
10 | enablePatches()
11 | const ops: any = {
12 |   CHANGE: 'replace',
13 |   CREATE: 'add',
14 |   REMOVE: 'remove',
15 | }
16 | let cloneData = ''
17 | onmessage = async (e) => {
18 |   if (!e.data) {
19 |     return
20 |   }
21 |   if (e.data.op === 'done') {
22 |     if (!cloneData) return
23 |     let fork = JSON.parse(cloneData)
24 |     let curArray = JSON.parse(e.data.data)
25 |     // 比较数据差异
26 |     let diffData: any = diff(fork, curArray)
27 |     // 生成差分补丁
28 |     fork = produce(
29 |       fork,
30 |       (draft) => {
31 |         for (const d of diffData) {
32 |           d.op = ops[d.type]
33 |         }
34 |         draft = applyPatches(draft, diffData)
35 |       },
36 |       (patches, inversePatches) => {
37 |         self.postMessage({ patches, inversePatches })
38 |       },
39 |     )
40 |     cloneData = ''
41 |   } else cloneData = e.data.data
42 | }
43 | 


--------------------------------------------------------------------------------
/src/utils/widgets/loadFontRule.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2023-08-23 17:37:16
 4 |  * @Description: 提取字体子集
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-12-28 19:52:55
 7 |  */
 8 | /**
 9 |  * 只有ttf/otf这种原始字体支持提取,如果服务端不支持该功能请设置false,以保证页面能加载字体。
10 |  */
11 | import _config from '@/config'
12 | export const fontMinWithDraw = _config.supportSubFont // true 开启,false 关闭
13 | 
14 | import api from '@/api'
15 | import { blob2Base64, generateFontStyle } from '@/common/methods/fonts/utils'
16 | 
17 | export const font2style = async (fontContent: any, fontData: any = []) => {
18 |   return new Promise((resolve: Function) => {
19 |     Promise.all(
20 |       Object.keys(fontContent).map(async (key) => {
21 |         const font = fontData.find((font: any) => font.value === key) as any
22 |         if (font.id) {
23 |           const extra = font.oid ? {} : { responseType: 'blob' }
24 |           const params = {
25 |             font_id: font.oid,
26 |             id: font.id,
27 |             content: shortText(fontContent[key]),
28 |           }
29 |           try {
30 |             const result = await api.material.getFontSub(params, extra)
31 |             fontContent[key] = font.oid ? result : await blob2Base64(result as Blob)
32 |           } catch (e) {
33 |             console.log('字体获取失败', e)
34 |           }
35 |         }
36 |       }),
37 |     ).then(() => {
38 |       const fontStyles = Object.keys(fontContent).reduce((pre, cur) => pre + generateFontStyle(cur, fontContent[cur]).outerHTML, '')
39 |       document.head.innerHTML += fontStyles
40 |       // document.head.appendChild(fontStyles)
41 |       resolve()
42 |     })
43 |   })
44 | }
45 | 
46 | function shortText(text: string) {
47 |   // 文字去重
48 |   const textArr = Array.from(new Set(text.split('')))
49 |   return textArr.join('')
50 | }
51 | 


--------------------------------------------------------------------------------
/src/views/components/Folder.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-03 19:15:21
 4 |  * @Description: 文件 
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-10 07:16:00
 7 | -->
 8 | <template>
 9 |   <el-dropdown trigger="click" size="large" placement="bottom-start">
10 |     <span class="el-dropdown-link">
11 |       <slot />
12 |     </span>
13 |     <template #dropdown>
14 |       <el-dropdown-menu>
15 |         <el-dropdown-item><div @click="$emit('select', 'newDesign')" class="item">创建设计</div></el-dropdown-item>
16 |         <el-dropdown-item @click="openPSD">导入文件</el-dropdown-item>
17 |         <el-dropdown-item @click="$emit('select', 'save')" divided>保存</el-dropdown-item>
18 |         <el-dropdown-item @click="$emit('select', 'download')">导出文件</el-dropdown-item>
19 |         <el-dropdown-item disabled>版本记录</el-dropdown-item>
20 |         <el-dropdown-item disabled>批量套模板</el-dropdown-item>
21 |         <el-dropdown-item @click="$emit('select', 'changeLineGuides')" divided>标尺与参考线</el-dropdown-item>
22 |       </el-dropdown-menu>
23 |     </template>
24 |   </el-dropdown>
25 | </template>
26 | 
27 | <script setup lang="ts">
28 | // import { ref, Ref } from 'vue'
29 | import { useRouter } from 'vue-router'
30 | import { ElDropdown, ElDropdownItem, ElDropdownMenu } from 'element-plus'
31 | 
32 | const router = useRouter()
33 | 
34 | const openPSD = () => {
35 |   window.open(router.resolve('/psd').href, '_blank')
36 | }
37 | </script>
38 | 
39 | <style lang="less" scoped>
40 | .item {
41 |   width: 224px;
42 | }
43 | </style>
44 | 


--------------------------------------------------------------------------------
/src/views/components/Tour.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-04 03:05:45
 4 |  * @Description: 漫游导航
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-05 05:31:01
 7 | -->
 8 | <template>
 9 |   <el-tour v-model="isShow">
10 |     <el-tour-step :target="steps[0]?.$el" title="文件管理">
11 |       <div>点击文件菜单,管理你的设计,设置页面视图等操作。</div>
12 |     </el-tour-step>
13 |     <el-tour-step placement="right" :target="steps[1]?.$el" title="左侧工具栏" description="在这里可以选择模板开始设计,或是挑选文字、图片等素材拖拽至画布中。" />
14 |     <el-tour-step placement="left" :target="steps[2]?.$el" title="右侧属性栏" description="当选中画布中的元素时,在此处会显示相应的编辑界面;同时也可以切换到“图层”管理。" />
15 |     <el-tour-step :target="steps[3]?.$el" title="下载作品" description="点击此处即可导出当前作品,赶紧试试吧。" />
16 |   </el-tour>
17 | </template>
18 | 
19 | <script setup lang="ts">
20 | import { ElTour, ElTourStep } from 'element-plus'
21 | import { ref } from 'vue'
22 | 
23 | type TProps = {
24 |   steps: any,
25 | }
26 | 
27 | const props = withDefaults(defineProps<TProps>(), {
28 |   steps: [],
29 | })
30 | 
31 | const isShow = ref(false)
32 | 
33 | const open = () => {
34 |   isShow.value = true
35 | }
36 | 
37 | defineExpose({
38 |   open
39 | })
40 | </script>
41 | 


--------------------------------------------------------------------------------
/src/views/components/Watermark.vue:
--------------------------------------------------------------------------------
 1 | <!--
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2024-04-08 16:50:04
 4 |  * @Description: 画布加水印
 5 |  * @LastEditors: ShawnPhang <https://m.palxp.cn>
 6 |  * @LastEditTime: 2024-04-08 18:00:37
 7 | -->
 8 | <template>
 9 |   <el-switch v-model="wmBollean" @change="wmChange" size="large" inline-prompt style="--el-switch-off-color: #9999999e" active-text="移除水印" inactive-text="官方水印" />
10 | </template>
11 | 
12 | <script setup lang="ts">
13 | import { ref } from 'vue'
14 | import { useBaseStore } from '@/store'
15 | 
16 | const baseStore = useBaseStore()
17 | const wmBollean = ref(false)
18 | 
19 | function wmChange(isRemove: string | number | boolean) {
20 |   baseStore.changeWatermark(isRemove ? '' : ['迅排设计', 'poster-design'])
21 | }
22 | </script>
23 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "esnext",
 4 |     "module": "esnext",
 5 |     "strict": true,
 6 |     "jsx": "preserve",
 7 |     "importHelpers": true,
 8 |     "moduleResolution": "node",
 9 |     "experimentalDecorators": true,
10 |     "skipLibCheck": true,
11 |     "esModuleInterop": true,
12 |     "allowSyntheticDefaultImports": true,
13 |     "sourceMap": true,
14 |     "baseUrl": ".",
15 |     "types": ["webpack-env", "element-plus/global"],
16 |     "paths": {
17 |       "@/*": ["src/*"]
18 |     },
19 |     "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
20 |     "resolveJsonModule": true
21 |   },
22 |   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
23 |   "exclude": ["node_modules"]
24 | }
25 | 


--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
 1 | /*
 2 |  * @Author: ShawnPhang
 3 |  * @Date: 2021-08-19 18:30:38
 4 |  * @Description: Vite配置文件
 5 |  * @LastEditors: ShawnPhang <site: book.palxp.com>
 6 |  * @LastEditTime: 2023-08-01 10:46:59
 7 |  */
 8 | import { defineConfig } from 'vite'
 9 | import vue from '@vitejs/plugin-vue'
10 | import path from 'path'
11 | import viteCompression from 'vite-plugin-compression'
12 | import ElementPlus from 'unplugin-element-plus/vite'
13 | 
14 | const resolve = (...data: string[]) => path.resolve(__dirname, ...data)
15 | 
16 | // https://vitejs.dev/config/
17 | export default defineConfig({
18 |   // base: '/web',
19 |   plugins: [
20 |     vue(),
21 |     viteCompression({
22 |       verbose: true,
23 |       disable: false,
24 |       threshold: 10240,
25 |       algorithm: 'gzip',
26 |       ext: '.gz',
27 |     }),
28 |     ElementPlus({
29 |       // options
30 |     }),
31 |   ],
32 |   build: {
33 |     minify: 'terser',
34 |     terserOptions: {
35 |       compress: {
36 |         drop_console: true,
37 |         drop_debugger: true,
38 |       },
39 |     },
40 |   },
41 |   resolve: {
42 |     alias: {
43 |       '@': resolve('src'),
44 |       '~data': resolve('src/assets/data'),
45 |     },
46 |   },
47 |   css: {
48 |     preprocessorOptions: {
49 |       less: {
50 |         modifyVars: {
51 |           color: `true; @import "./src/assets/styles/color.less";`,
52 |         },
53 |       },
54 |     },
55 |   },
56 |   define: {
57 |     'process.env': process.env,
58 |   },
59 |   server: {
60 |     hmr: { overlay: false },
61 |     host: '127.0.0.1'
62 |     // proxy: {
63 |     //   '/api': {
64 |     //     target: '',
65 |     //     changeOrigin: true,
66 |     //     rewrite: (path) => path.replace(/^\/api/, ''),
67 |     //   },
68 |     // },
69 |   },
70 | })
71 | 


--------------------------------------------------------------------------------