├── .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 | --------------------------------------------------------------------------------