diff --git a/README.md b/README.md index 7f59d3b6..268dcb5e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,9 @@ # 🎨 PPTist -> 一个基于 Vue3.x + TypeScript 的在线演示文稿应用,还原了大部分PPT常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型,每一种元素都拥有高度可编辑能力,同时支持丰富的快捷键和右键菜单,尽可能还原本地桌面应用的使用体验。 +> 一个基于 Vue3.x + TypeScript 的在线演示文稿(幻灯片)应用,还原了大部分 Office PowerPoint 常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型,每一种元素都拥有高度可编辑能力,同时支持丰富的快捷键和右键菜单,尽可能还原本地桌面应用的使用体验。 在线体验地址:[https://pipipi-pikachu.github.io/PPTist/](https://pipipi-pikachu.github.io/PPTist/) -如果网络状态不佳,可以访问国内镜像:[https://pptist.gitee.io/](https://pptist.gitee.io/) - -Github仓库:[https://github.com/pipipi-pikachu/PPTist](https://github.com/pipipi-pikachu/PPTist) +如果网络状态不佳,可以访问国内镜像(非实时更新):[https://pptist.gitee.io/](https://pptist.gitee.io/) # 🚀 项目运行 @@ -30,6 +28,7 @@ npm run serve - 画布缩放 - 主题设置 - 幻灯片备注 +- 幻灯片模板 ### 幻灯片元素编辑 - 元素添加、删除 - 元素复制粘贴 @@ -93,7 +92,8 @@ npm run serve - 翻页动画 - 元素入场动画 - 全部幻灯片预览 -- 画笔工具 +- 画笔、黑板工具 +- 自动放映 # 💡 常见问题 @@ -125,6 +125,16 @@ A. 设置预置主题的作用是使新添加的元素和页面应用主题样 A. 设置在线字体时会下载对应的字体文件,该文件较大,需要等待下载完成后才会应用新的字体。 +**Q. 关于导入导出PPTX文件** + +A. 作为一个在线幻灯片应用,导出、导入PPTX文件是非常重要的功能,但是经过调研发现,该功能实现起来的复杂度远超过了预期。由于个人能力和时间有限,这部分功能只能借助第三方的轮子来完成。 + +目前导出功能主要基于 [PptxGenJS](https://github.com/gitbrent/PptxGenJS/) 完成,能够实现大多数基本元素的导出,但还有非常多的缺陷需要一点点完善。同时需要知晓的是:1、该功能依赖 PptxGenJS,对于该库本身无法实现的部分,我也无能为力;2、导出功能的目标只是【导出样式尽可能一致的元素】,而不是一比一将网页还原到PPT,一些样式差异是必然存在的。 + +导入功能目前暂时没有合适的解决方案,还在调研和观望中。 + +如果有感兴趣或做过相关内容的朋友,欢迎在 [issues](https://github.com/pipipi-pikachu/PPTist/issues/57) 中讨论。 + # 📁 项目目录结构 ``` @@ -162,15 +172,6 @@ A. 设置在线字体时会下载对应的字体文件,该文件较大,需 具体类型的定义可见:[https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts](https://github.com/pipipi-pikachu/PPTist/blob/master/src/types/slides.ts) -# 📃 TODO -- [ ] 幻灯片模板 -- [ ] 图表缩略图优化 -- [ ] 公式元素 -- [ ] 导出、导入 - -> 作为一个在线幻灯片,导出、导入PPTX文件是非常重要的功能。但是经过本人一段时间的调研发现,该功能实现起来的复杂度远超过了预期。由于个人时间有限,暂时可能无法集中精力来做该功能,短时间内还是会更多优先去提升其他方面的使用体验。如果有做过相关内容的朋友,也欢迎给我提建议。 - - # 💻 贡献代码 首先感谢每一位关注本项目的朋友,由于本人时间精力有限,且目前项目规模不大,暂时没有团队化开发维护本项目的打算。但非常欢迎每一位对本项目感兴趣的朋友贡献代码。 ### 具体参考如下: diff --git a/package-lock.json b/package-lock.json index d29d3bba..f164846b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2479,6 +2479,11 @@ "integrity": "sha1-mqMMBNshKpoGSdaub9UKzMQHSKE=", "dev": true }, + "@types/svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-3h04sJhF2rjOq8zUhyomORyKdr0RUts7FAz/JajBKGpTF0JSXjaj9fjWtAqj+pU1fwsGsHzcm3Neew3t/McUXA==" + }, "@types/tapable": { "version": "1.0.7", "resolved": "https://registry.npm.taobao.org/@types/tapable/download/@types/tapable-1.0.7.tgz", @@ -6341,8 +6346,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/core-util-is/download/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "7.0.0", @@ -9567,6 +9571,11 @@ "sshpk": "^1.7.0" } }, + "https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=" + }, "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npm.taobao.org/https-browserify/download/https-browserify-1.0.0.tgz", @@ -9706,6 +9715,11 @@ "dev": true, "optional": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npm.taobao.org/import-cwd/download/import-cwd-2.1.0.tgz", @@ -9848,8 +9862,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npm.taobao.org/inherits/download/inherits-2.0.4.tgz", - "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", - "dev": true + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" }, "ini": { "version": "1.3.8", @@ -10363,8 +10376,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -12381,6 +12393,46 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz", @@ -12583,6 +12635,14 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npm.taobao.org/lines-and-columns/download/lines-and-columns-1.1.6.tgz", @@ -14155,8 +14215,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npm.taobao.org/pako/download/pako-1.0.11.tgz?cache=0&sync_timestamp=1610208924901&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpako%2Fdownload%2Fpako-1.0.11.tgz", - "integrity": "sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8=", - "dev": true + "integrity": "sha1-bJWZ00DVTf05RjgCUqNXBaa5kr8=" }, "parallel-transform": { "version": "1.2.0", @@ -15257,6 +15316,32 @@ "integrity": "sha1-RD9qIM7WSBor2k+oUypuVdeJoss=", "dev": true }, + "pptxgenjs": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.7.0.tgz", + "integrity": "sha512-ZtKT11DA3arRhyQ0AJoaa4iizHEghfreobQhbDq9EVtJhZ0JN3it/lREnvervuHinHi6sGBcSlHC6scfubEm7Q==", + "requires": { + "@types/node": "^16.0.0", + "https": "^1.0.0", + "image-size": "^1.0.0", + "jszip": "^3.6.0" + }, + "dependencies": { + "@types/node": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.0.tgz", + "integrity": "sha512-HrJuE7Mlqcjj+00JqMWpZ3tY8w7EUd+S0U3L1+PQSWiXZbOgyQDvi+ogoUxaHApPJq5diKxYBQwA3iIlNcPqOg==" + }, + "image-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz", + "integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==", + "requires": { + "queue": "6.0.2" + } + } + } + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npm.taobao.org/prelude-ls/download/prelude-ls-1.1.2.tgz", @@ -15361,8 +15446,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npm.taobao.org/process-nextick-args/download/process-nextick-args-2.0.1.tgz", - "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=", - "dev": true + "integrity": "sha1-eCDZsWEgzFXKmud5JoCufbptf+I=" }, "progress": { "version": "2.0.3", @@ -15632,6 +15716,14 @@ "integrity": "sha1-M0WUG0FTy50ILY7uTNogFqmu9/Y=", "dev": true }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "requires": { + "inherits": "~2.0.3" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npm.taobao.org/queue-microtask/download/queue-microtask-1.2.3.tgz?cache=0&sync_timestamp=1616391548624&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fqueue-microtask%2Fdownload%2Fqueue-microtask-1.2.3.tgz", @@ -16483,6 +16575,11 @@ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npm.taobao.org/set-value/download/set-value-2.0.1.tgz", @@ -17714,6 +17811,16 @@ "has-flag": "^4.0.0" } }, + "svg-arc-to-cubic-bezier": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==" + }, + "svg-pathdata": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.0.tgz", + "integrity": "sha512-8XoCZjKbP0fvJbDsm6KFxVau2N3SkWkMj8OniADm87q4OiFXk/gSgri5Uyr8hE1fQ9npI+9XzRlTUObgWmBBNw==" + }, "svg-tags": { "version": "1.0.0", "resolved": "https://registry.npm.taobao.org/svg-tags/download/svg-tags-1.0.0.tgz", @@ -18949,8 +19056,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npm.taobao.org/util-deprecate/download/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.1.1", diff --git a/package.json b/package.json index 348e241d..e1f245cf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "file-saver": "^2.0.5", "lodash": "^4.17.20", "mitt": "^3.0.0", + "pptxgenjs": "^3.7.0", "prosemirror-commands": "^1.1.7", "prosemirror-dropcursor": "^1.3.2", "prosemirror-gapcursor": "^1.1.5", @@ -30,6 +31,8 @@ "prosemirror-schema-list": "^1.1.4", "prosemirror-state": "^1.3.3", "prosemirror-view": "^1.18.1", + "svg-arc-to-cubic-bezier": "^3.2.0", + "svg-pathdata": "^6.0.0", "tinycolor2": "^1.4.2", "vue": "^3.1.4", "vuedraggable": "^4.0.1", @@ -52,6 +55,7 @@ "@types/prosemirror-schema-basic": "^1.0.1", "@types/prosemirror-schema-list": "^1.0.1", "@types/resize-observer-browser": "^0.1.4", + "@types/svg-arc-to-cubic-bezier": "^3.2.0", "@types/tinycolor2": "^1.4.2", "@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/parser": "^4.23.0", diff --git a/src/components/FullscreenSpin.vue b/src/components/FullscreenSpin.vue new file mode 100644 index 00000000..cce5db8b --- /dev/null +++ b/src/components/FullscreenSpin.vue @@ -0,0 +1,34 @@ + + + + + \ No newline at end of file diff --git a/src/configs/shapes.ts b/src/configs/shapes.ts index 4482f289..05d3f309 100644 --- a/src/configs/shapes.ts +++ b/src/configs/shapes.ts @@ -1,6 +1,7 @@ export interface ShapePoolItem { viewBox: number; path: string; + special?: boolean; } export const SHAPE_LIST = [ @@ -282,10 +283,12 @@ export const SHAPE_LIST = [ { viewBox: 1024, path: 'M398.208 302.912V64L0 482.112l398.208 418.176V655.36c284.48 0 483.584 95.552 625.792 304.64-56.896-298.688-227.584-597.312-625.792-657.088z', + special: true, }, { viewBox: 1024, path: 'M625.792 302.912V64L1024 482.112l-398.208 418.176V655.36C341.312 655.36 142.208 750.912 0 960c56.896-298.688 227.584-597.312 625.792-657.088z', + special: true, }, ], }, @@ -296,66 +299,82 @@ export const SHAPE_LIST = [ { viewBox: 1024, path: 'M995.336 243.4016c-15.7584-36.5736-38.3376-69.26639999-66.91440001-97.37280001-28.5768-27.98879999-61.73999999-49.8624-98.78399999-65.26799998-38.22-15.876-78.6744-23.8728-120.4224-23.87280001-57.97680001 0-114.5424 15.876-163.69919999 45.864-11.76 7.17360001-22.932 15.05279999-33.51600001 23.63760001-10.584-8.5848-21.75600001-16.46400001-33.51600001-23.63760001-49.1568-29.98799999-105.7224-45.86399999-163.69919999-45.864-41.74799999 0-82.2024 7.9968-120.4224 23.87280001-36.9264 15.28799999-70.2072 37.27919999-98.78399999 65.26799998-28.6944 28.10640001-51.156 60.79919999-66.91440001 97.37280001-16.34639999 37.9848-24.696 78.3216-24.696 119.83439999 0 39.1608 7.9968 79.96800001 23.8728 121.48080001 13.28880001 34.692 32.34000001 70.67760001 56.6832 107.016 38.57279999 57.5064 91.61040001 117.4824 157.4664 178.28160001 109.1328 100.78319999 217.2072 170.4024 221.79359999 173.22479998l27.87120001 17.8752c12.348 7.8792 28.224 7.8792 40.572 0l27.87119999-17.8752c4.58639999-2.94 112.54319999-72.44159999 221.79360001-173.22479998 65.85599999-60.79919999 118.89359999-120.7752 157.4664-178.28160001 24.3432-36.33839999 43.512-72.324 56.68319999-107.016 15.876-41.5128 23.8728-82.32 23.87280001-121.48080001 0.1176-41.5128-8.232-81.8496-24.5784-119.83439999z', + special: true, }, { viewBox: 1024, path: 'M985.20746667 343.50079998l-303.32586667-44.08319999L546.28693333 24.5248c-3.70346666-7.5264-9.79626667-13.6192-17.32266665-17.32266668-18.87573334-9.3184-41.81333333-1.55306667-51.25120001 17.32266668L342.1184 299.41759999l-303.32586667 44.08319999c-8.36266667 1.19466667-16.00853333 5.13706667-21.8624 11.11040001-14.69440001 15.17226667-14.45546667 39.30453334 0.71679999 54.1184l219.46026668 213.9648-51.84853333 302.1312c-1.43359999 8.24320001-0.11946667 16.8448 3.82293333 24.25173333 9.79626667 18.6368 32.9728 25.92426667 51.6096 16.00853334L512 822.44266665l271.3088 142.64320001c7.40693333 3.9424 16.00853333 5.25653333 24.25173333 3.82293333 20.78719999-3.584 34.7648-23.296 31.1808-44.0832l-51.84853333-302.1312 219.46026668-213.9648c5.97333334-5.85386666 9.91573333-13.49973334 11.11039999-21.8624 3.2256-20.90666667-11.34933333-40.26026667-32.256-43.36640001z', + special: true, }, { viewBox: 1024, path: 'M852.65066667 405.84533333C800.54044445 268.40177778 667.76177778 170.66666667 512.22755555 170.66666667S223.91466667 268.288 171.80444445 405.73155555C74.29688889 431.33155555 2.27555555 520.07822222 2.27555555 625.77777778c0 125.72444445 101.83111111 227.55555555 227.44177778 227.55555555h564.56533334C919.89333333 853.33333333 1021.72444445 751.50222222 1021.72444445 625.77777778c0-105.472-71.79377778-194.21866667-169.07377778-219.93244445z', + special: true, }, { viewBox: 1024, path: 'M926.25224691 323.7371485H654.6457886L898.88200917 15.14388241c5.05486373-6.53433603 0.49315743-16.02761669-7.76722963-16.02761668H418.30008701c-3.45210206 0-6.78091476 1.84934039-8.50696579 4.93157436L90.35039154 555.76772251c-3.82197013 6.53433603 0.86302552 14.7947231 8.50696578 14.79472311h215.01664245l-110.22068713 440.88274851c-2.34249783 9.61657002 9.24670194 16.39748478 16.39748477 9.49328065L933.03316167 340.62779071c6.41104668-6.0411786 2.09591911-16.8906422-6.78091476-16.89064221z', + special: true, }, { viewBox: 1024, path: 'M878.47822222 463.30311111c-22.18666667-49.83466667-53.93066667-93.98044445-94.32177777-131.072l-33.10933334-30.37866666c-4.89244445-4.32355555-12.62933333-2.38933333-14.79111111 3.75466666l-14.79111111 42.43911111c-9.216 26.624-26.16888889 53.81688889-50.176 80.55466667-1.59288889 1.70666667-3.41333333 2.16177778-4.66488889 2.27555556-1.25155555 0.11377778-3.18577778-0.11377778-4.89244445-1.70666667-1.59288889-1.36533333-2.38933333-3.41333333-2.27555555-5.46133333 4.20977778-68.49422222-16.27022222-145.74933333-61.09866667-229.83111112C561.26577778 124.01777778 509.72444445 69.51822222 445.32622222 31.51644445l-46.99022222-27.648c-6.144-3.64088889-13.99466667 1.13777778-13.65333333 8.30577777l2.50311111 54.61333333c1.70666667 37.31911111-2.61688889 70.31466667-12.85688889 97.73511112-12.51555555 33.56444445-30.49244445 64.73955555-53.47555556 92.72888888-16.15644445 19.56977778-34.24711111 37.20533333-54.04444444 52.45155556-47.90044445 36.75022222-87.38133333 84.65066667-114.11911111 138.24C125.72444445 502.10133333 111.50222222 562.74488889 111.50222222 623.50222222c0 53.70311111 10.58133333 105.69955555 31.51644445 154.73777778 20.25244445 47.21777778 49.152 89.77066667 85.90222222 126.17955555 36.864 36.40888889 79.64444445 65.08088889 127.31733333 84.992C405.61777778 1010.11911111 457.95555555 1020.58666667 512 1020.58666667s106.38222222-10.46755555 155.76177778-31.06133334c47.67288889-19.91111111 90.56711111-48.46933333 127.31733333-84.992 36.864-36.40888889 65.76355555-78.96177778 85.90222222-126.17955555 20.93511111-49.03822222 31.51644445-101.03466667 31.51644445-154.73777778 0-55.52355555-11.37777778-109.45422222-34.01955556-160.31288889z', + special: true, }, { viewBox: 1024, path: 'M968.20337778 20.11591112H705.44042667c-22.17301333 0-41.92483556 15.16430222-47.14951111 37.33731555C642.36202666 124.73685332 582.08711111 173.03324444 512 173.03324444s-130.36202666-48.29639112-146.29091556-115.58001777c-5.22467555-22.17301333-24.84906667-37.33731556-47.14951111-37.33731555H55.79662222c-30.96576 0-56.06968889 25.10392889-56.06968888 56.06968888v321.12639999c0 30.96576 25.10392889 56.06968889 56.06968888 56.06968889h95.57333334v494.43271112c0 30.96576 25.10392889 56.06968889 56.06968889 56.06968888h609.1207111c30.96576 0 56.06968889-25.10392889 56.06968889-56.06968888V453.38168888h95.57333334c30.96576 0 56.06968889-25.10392889 56.06968888-56.06968889V76.1856c0-30.96576-25.10392889-56.06968889-56.06968888-56.06968888z', + special: true, }, { viewBox: 1024, path: 'M980.94648889 239.80714666H523.46880001L373.99210666 96.82944c-1.91146667-1.78403556-4.46008889-2.80348444-7.00871111-2.80348445H43.05351111c-22.55530667 0-40.77795555 18.22264888-40.77795555 40.77795557v754.39217776c0 22.55530667 18.22264888 40.77795555 40.77795555 40.77795557h937.89297778c22.55530667 0 40.77795555-18.22264888 40.77795555-40.77795557V280.58510222c0-22.55530667-18.22264888-40.77795555-40.77795555-40.77795556z', + special: true, }, { viewBox: 1024, path: 'M972.60904597 164.57058577L841.30587843 33.39070759c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480282-29.3428675-26.75379095 0-51.90482023 10.47959553-70.76809219 29.3428675L558.60337778 174.68031322c-18.86327195 18.86327195-29.34286748 44.1375906-29.34286749 70.64480283 0 26.75379095 10.47959553 51.90482023 29.34286749 70.76809218l103.31648301 103.31648302c-24.28800376 53.50758189-57.69942011 101.59043198-99.24793416 143.13894603-41.42522469 41.67180341-89.63136414 75.08321976-143.13894603 99.61780223L316.21649759 558.84995649c-18.86327195-18.86327195-44.1375906-29.34286748-70.64480283-29.34286747-26.75379095 0-51.90482023 10.47959553-70.76809217 29.34286747L33.39070759 700.01627278c-18.86327195 18.86327195-29.34286748 44.1375906-29.3428675 70.76809217 0 26.75379095 10.47959553 51.90482023 29.3428675 70.76809219l131.05658883 131.05658883c30.08260365 30.205893 71.63111769 47.34311394 114.28923598 47.34311394 9.00012323 0 17.63037836-0.73973616 26.13734414-2.21920846 166.19405621-27.37023774 331.03192945-115.76870829 464.06114804-248.67463751C901.84095379 636.27567408 990.11613498 471.56109018 1017.85624079 304.87387654c8.38367642-50.91850535-8.50696579-103.31648302-45.24719482-140.30329077z', + special: true, }, { viewBox: 1024, path: 'M910.60451556 640.96028445c-20.38897778-65.49959112-43.83630221-120.54983112-79.89930667-210.64362666C836.31217778 193.67708444 737.93535999 2.27555556 511.36284444 2.27555556 282.24170667 2.27555556 186.03121778 197.50001778 192.14791111 430.31665779c-36.19043555 90.22122667-59.51032888 144.88917333-79.89930667 210.64362666-43.32657778 139.53706668-29.30915556 197.26336001-18.60494222 198.53767111 22.9376 2.80348444 89.32920888-105.00323556 89.32920889-105.00323556 0 62.44124445 32.11264001 143.86972444 101.69002667 202.61546667-33.64181333 10.32192-109.20846222 38.10190221-91.24067556 68.55793777 14.52714667 24.59420444 250.01984 15.67402668 317.94062222 8.02816 67.92078222 7.64586667 303.41347556 16.56604444 317.94062223-8.02816 17.96778667-30.32860444-57.72629333-58.23601779-91.24067555-68.55793777 69.57738667-58.87317334 101.69002667-140.30165333 101.69002667-202.61546667 0 0 66.39160889 107.80672 89.32920888 105.00323556 10.83164445-1.40174222 24.84906667-59.12803556-18.47751111-198.53767111z', + special: true, }, { viewBox: 1024, path: 'M1016.86992592 199.24764445c-37.13706667 16.01991111-77.55093333 27.54939259-119.17842962 32.03982222 42.96248889-25.60758518 75.60912592-66.02145185 91.02222222-114.08118519-39.68568889 23.66577778-84.58998518 41.02068148-131.31472593 50.00154074C819.53374815 126.79395555 765.76995555 101.79318518 706.18074075 101.79318518c-114.688 0-206.92385185 92.96402963-206.92385186 207.04521482 0 16.01991111 1.94180741 32.03982222 5.09724444 47.45291852-171.72859259-8.98085925-324.88865185-91.02222222-426.71217778-216.63288889-17.96171852 30.82619259-28.15620741 66.02145185-28.1562074 104.49351112 0 71.84687408 36.53025185 135.19834075 92.23585185 172.45677036-33.98162963-1.33499259-66.02145185-10.92266667-93.57084445-26.33576296v2.54862222c0 100.6098963 71.1186963 183.98625185 165.90317037 203.1616-17.3549037 4.49042963-35.92343703 7.03905185-54.49197037 7.03905185-13.47128889 0-26.2144-1.33499259-39.07887407-3.15543704C146.69748148 681.90814815 223.03478518 741.49736297 313.93564445 743.43917037c-71.1186963 55.7056-160.19911111 88.4736-256.9253926 88.4736-17.3549037 0-33.37481482-0.60681482-50.00154074-2.54862222C98.75911111 888.22518518 207.62168889 922.20681482 324.85831111 922.20681482 705.45256297 922.20681482 913.71140741 606.90583703 913.71140741 333.23235555c0-8.98085925 0-17.96171852-0.60681482-26.94257777 40.2925037-29.4912 75.60912592-66.02145185 103.76533333-107.04213333z', + special: true, }, { viewBox: 1024, path: 'M917.96720197 1.08889505H106.03279803C53.56084718 1.08889505 9.37393998 45.27580225 9.37393998 97.74775309v5.52336372c0 19.33177108 8.28504494 41.42522469 22.0934536 55.23363205l331.40179753 392.15879462v325.87843379c0 16.57008987 8.28504494 30.37849854 22.09345359 35.90186098l209.88780469 104.94390299 2.76168121 2.76168121c27.61681602 11.04672615 55.23363335-8.28504494 55.23363335-38.66354218V550.66354348l331.40179753-392.15879462c35.90186097-41.42522469 30.37849854-102.18222047-11.04672616-135.32240022-11.04672615-13.80840865-33.14017975-22.0934536-55.23363335-22.09345359z', + special: true, }, { viewBox: 1024, path: 'M491.70164031 97.48884502a25.89076502 25.89076502 0 0 1 40.59671938 0L745.66415762 367.01171317a25.89076502 25.89076502 0 0 0 30.49932208 7.72839349l208.00640948-89.14190458a25.89076502 25.89076502 0 0 1 35.56096592 29.06238339l-115.18801541 554.96855704A103.56306132 103.56306132 0 0 1 803.14165689 952.14301275H220.85834311a103.56306132 103.56306132 0 0 1-101.4011828-82.51387024l-115.18801541-554.96855704a25.89076502 25.89076502 0 0 1 35.54802012-29.06238339l208.01935528 89.14190458a25.89076502 25.89076502 0 0 0 30.49932208-7.72839349l213.36579793-269.52286815z', + special: true, }, { viewBox: 1024, path: 'M643.02466884 387.7801525c19.85376751-88.69205333 33.718272-152.84087467 41.61900049-192.57389433C704.52292267 95.17283515 652.90057916 2.27555515 550.58614084 2.27555515c-92.26012484 0-138.59407685 45.84971417-165.91530666 137.49816969l-0.70087152 2.67605334c-16.40038399 74.13942085-41.47882668 131.61085116-74.6746315 172.73287031a189.06953915 189.06953915 0 0 1-143.04142182 70.44391902l-26.17434983 0.5606965C77.66380049 387.52529067 27.76177817 438.90551468 27.76177817 501.84374084V881.55022182c0 77.4144 62.25009818 140.17422182 139.05282766 140.17422303h492.82707951c101.23127467 0 191.59267516-63.995904 225.93535999-159.98976l102.37815468-286.22301868c26.04691951-72.82688-11.39234134-153.15945284-83.63303784-179.42300483a138.04612267 138.04612267 0 0 0-47.17499733-8.30850884H643.02466884z', + special: true, }, { viewBox: 1024, path: 'M512 512c140.82958222 0 254.86222222-114.03264 254.86222222-254.86222222S652.82958222 2.27555555 512 2.27555555a254.78940445 254.78940445 0 0 0-254.86222222 254.86222223C257.13777778 397.96736 371.17041778 512 512 512z m0 72.81777778c-170.10232889 0-509.72444445 97.57582222-509.72444445 291.27111111v145.63555556h1019.4488889v-145.63555556c0-193.69528889-339.62211555-291.27111111-509.72444445-291.27111111z', + special: true, }, { viewBox: 1024, path: 'M1019.81297778 564.50161779l-138.89991111-472.51456c-8.66531556-25.99594668-29.43658667-43.45400889-57.21656889-43.45400891s-50.33528889 15.67402668-59.00060446 41.66997334l-92.00526221 274.48661334H351.69166222L259.6864 90.33045333c-8.66531556-25.99594668-31.22062222-41.66997333-59.00060444-41.66997332s-50.33528889 17.33063112-57.2165689 43.45400887L4.69674667 564.50161779c-5.22467555 17.33063112 1.78403556 36.44529778 15.67402667 46.89464887l491.11950221 368.27591113 492.77610666-368.27591113c13.76256-10.32192 20.77127111-29.43658667 15.54659557-46.89464887z', + special: true, }, { viewBox: 1024, path: 'M927.78951111 340.39277037c-12.01493333-47.81700741 12.01493333-124.03294815 89.08041481-150.97552592l-82.40545184-4.36906667s-31.19028148-109.22666667-174.27721483-118.9357037c-143.08693333-9.8304-236.65777778-3.64088889-236.65777777-3.6408889s106.07122963 67.47780741 63.5941926 187.74850371c-31.06891852 63.71555555-79.85682963 116.02299259-132.04290371 175.61220741-1.57771852 1.57771852-3.03407408 3.15543703-4.2477037 4.49042962C278.25493333 624.86755555 7.13007408 934.34311111 7.13007408 934.34311111c298.43152592 78.15774815 498.43768889-7.64586667 616.76657777-110.56165926 24.87940741-0.24272592 43.5693037-0.36408889 56.19105185-0.36408888 164.8109037 0 304.13558518-142.72284445 298.43152593-301.4656-3.88361482-109.1053037-38.71478518-133.74198518-50.72971852-181.5589926z', + special: true, }, ], }, diff --git a/src/hooks/useCreateElement.ts b/src/hooks/useCreateElement.ts index c6060866..d520e10e 100644 --- a/src/hooks/useCreateElement.ts +++ b/src/hooks/useCreateElement.ts @@ -3,7 +3,7 @@ import { MutationTypes, useStore } from '@/store' import { createRandomCode } from '@/utils/common' import { getImageSize } from '@/utils/image' import { VIEWPORT_SIZE } from '@/configs/canvas' -import { PPTLineElement, ChartType, PPTElement, TableCell, TableCellStyle } from '@/types/slides' +import { PPTLineElement, ChartType, PPTElement, TableCell, TableCellStyle, PPTShapeElement } from '@/types/slides' import { ShapePoolItem } from '@/configs/shapes' import { LinePoolItem } from '@/configs/lines' import useHistorySnapshot from '@/hooks/useHistorySnapshot' @@ -176,7 +176,7 @@ export default () => { */ const createShapeElement = (position: CommonElementPosition, data: ShapePoolItem) => { const { left, top, width, height } = position - createElement({ + const newElement: PPTShapeElement = { type: 'shape', id: createRandomCode(), left, @@ -188,7 +188,9 @@ export default () => { fill: themeColor.value, fixedRatio: false, rotate: 0, - }) + } + if (data.special) newElement.special = true + createElement(newElement) } /** diff --git a/src/hooks/useExport.ts b/src/hooks/useExport.ts new file mode 100644 index 00000000..ccd771a5 --- /dev/null +++ b/src/hooks/useExport.ts @@ -0,0 +1,453 @@ +import { computed, ref } from 'vue' +import { trim } from 'lodash' +import { saveAs } from 'file-saver' +import pptxgen from 'pptxgenjs' +import tinycolor from 'tinycolor2' +import { getElementRange } from '@/utils/element' +import { AST, toAST } from '@/utils/htmlParser' +import { SvgPoints, toPoints } from '@/utils/svgPathParser' +import { svg2Base64 } from '@/utils/svg2Base64' +import { useStore } from '@/store' +import { message } from 'ant-design-vue' + +export default () => { + const store = useStore() + const slides = computed(() => store.state.slides) + + const exporting = ref(false) + + const exportJSON = () => { + const blob = new Blob([JSON.stringify(slides.value)], { type: '' }) + saveAs(blob, 'pptist_slides.json') + } + + const formatColor = (_color: string) => { + const c = tinycolor(_color) + const alpha = c.getAlpha() + const color = alpha === 0 ? '#ffffff' : c.setAlpha(1).toHexString() + return { + alpha, + color, + } + } + + const formatHTML = (html: string) => { + const ast = toAST(html) + + const slices: pptxgen.TextProps[] = [] + const parse = (obj: AST[], baseStyleObj = {}) => { + for (const item of obj) { + if ('tagName' in item && ['div', 'ul', 'li', 'p'].includes(item.tagName) && slices.length) { + const lastSlice = slices[slices.length - 1] + if (!lastSlice.options) lastSlice.options = {} + lastSlice.options.breakLine = true + } + + const styleObj = { ...baseStyleObj } + const styleAttr = 'attributes' in item ? item.attributes.find(attr => attr.key === 'style') : null + if (styleAttr && styleAttr.value) { + const styleArr = styleAttr.value.split(';') + for (const styleItem of styleArr) { + const [_key, _value] = styleItem.split(': ') + const [key, value] = [trim(_key), trim(_value)] + if (key && value) styleObj[key] = value + } + } + + if ('tagName' in item) { + if (item.tagName === 'em') { + styleObj['font-style'] = 'italic' + } + if (item.tagName === 'strong') { + styleObj['font-weight'] = 'bold' + } + if (item.tagName === 'sup') { + styleObj['vertical-align'] = 'super' + } + if (item.tagName === 'sub') { + styleObj['vertical-align'] = 'sub' + } + } + + if ('tagName' in item && item.tagName === 'br') { + slices.push({ text: '', options: { breakLine: true } }) + } + else if ('content' in item) { + const text = item.content.replace(/\n/g, '').replace(/ /g, ' ') + const options: pptxgen.TextPropsOptions = {} + + if (styleObj['font-size']) { + options.fontSize = parseInt(styleObj['font-size']) * 0.75 + } + if (styleObj['color']) { + options.color = formatColor(styleObj['color']).color + } + if (styleObj['background-color']) { + options.highlight = formatColor(styleObj['background-color']).color + } + if (styleObj['text-decoration-line']) { + if (styleObj['text-decoration-line'].indexOf('underline') !== -1) { + options.underline = { + color: options.color || '#000000', + style: 'sng', + } + } + if (styleObj['text-decoration-line'].indexOf('line-through') !== -1) { + options.strike = 'sngStrike' + } + } + if (styleObj['text-decoration']) { + if (styleObj['text-decoration'].indexOf('underline') !== -1) { + options.underline = { + color: options.color || '#000000', + style: 'sng', + } + } + if (styleObj['text-decoration'].indexOf('line-through') !== -1) { + options.strike = 'sngStrike' + } + } + if (styleObj['vertical-align']) { + if (styleObj['vertical-align'] === 'super') options.superscript = true + if (styleObj['vertical-align'] === 'sub') options.subscript = true + } + if (styleObj['text-align']) options.align = styleObj['text-align'] + if (styleObj['font-weight']) options.bold = styleObj['font-weight'] === 'bold' + if (styleObj['font-style']) options.italic = styleObj['font-style'] === 'italic' + if (styleObj['font-family']) options.fontFace = styleObj['font-family'] + + slices.push({ text, options }) + } + else if ('children' in item) parse(item.children, styleObj) + } + } + parse(ast) + return slices + } + + type Points = Array< + | { x: number; y: number; moveTo?: boolean } + | { x: number; y: number; curve: { type: 'arc'; hR: number; wR: number; stAng: number; swAng: number } } + | { x: number; y: number; curve: { type: 'quadratic'; x1: number; y1: number } } + | { x: number; y: number; curve: { type: 'cubic'; x1: number; y1: number; x2: number; y2: number } } + | { close: true } + > + const formatPoints = (points: SvgPoints, scale = { x: 1, y: 1 }): Points => { + return points.map(point => { + if (point.close !== undefined) { + return { close: true } + } + else if (point.type === 'M') { + return { + x: point.x / 100 * scale.x, + y: point.y / 100 * scale.y, + moveTo: true, + } + } + else if (point.curve) { + if (point.curve.type === 'cubic') { + return { + x: point.x / 100 * scale.x, + y: point.y / 100 * scale.y, + curve: { + type: 'cubic', + x1: (point.curve.x1 as number) / 100 * scale.x, + y1: (point.curve.y1 as number) / 100 * scale.y, + x2: (point.curve.x2 as number) / 100 * scale.x, + y2: (point.curve.y2 as number) / 100 * scale.y, + }, + } + } + else if (point.curve.type === 'quadratic') { + return { + x: point.x / 100 * scale.x, + y: point.y / 100 * scale.y, + curve: { + type: 'quadratic', + x1: (point.curve.x1 as number) / 100 * scale.x, + y1: (point.curve.y1 as number) / 100 * scale.y, + }, + } + } + } + return { + x: point.x / 100 * scale.x, + y: point.y / 100 * scale.y, + } + }) + } + + const exportPPTX = () => { + exporting.value = true + const pptx = new pptxgen() + + for (const slide of slides.value) { + const pptxSlide = pptx.addSlide() + + if (slide.background) { + const background = slide.background + if (background.type === 'image' && background.image) { + pptxSlide.background = { data: background.image } + } + else if (background.type === 'solid' && background.color) { + const c = formatColor(background.color) + pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 } + } + else if (background.type === 'gradient' && background.gradientColor) { + const [color1, color2] = background.gradientColor + const color = tinycolor.mix(color1, color2).toHexString() + const c = formatColor(color) + pptxSlide.background = { color: c.color, transparency: (1 - c.alpha) * 100 } + } + } + + if (!slide.elements) continue + + for (const el of slide.elements) { + if (el.type === 'text') { + const textProps = formatHTML(el.content) + + const options: pptxgen.TextPropsOptions = { + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + fontSize: 20 * 0.75, + fontFace: '微软雅黑', + color: '#000000', + valign: 'middle', + } + if (el.rotate) options.rotate = el.rotate + if (el.wordSpace) options.charSpacing = el.wordSpace * 0.75 + if (el.lineHeight) options.lineSpacingMultiple = el.lineHeight * 0.75 + if (el.fill) { + const c = formatColor(el.fill) + const opacity = el.opacity === undefined ? 1 : el.opacity + options.fill = { color: c.color, transparency: (1 - c.alpha * opacity) * 100 } + } + if (el.defaultColor) options.color = formatColor(el.defaultColor).color + if (el.defaultFontName) options.fontFace = el.defaultFontName + + pptxSlide.addText(textProps, options) + } + else if (el.type === 'image') { + const options: pptxgen.ImageProps = { + path: el.src, + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + } + if (el.flipH) options.flipH = el.flipH + if (el.flipV) options.flipV = el.flipV + if (el.rotate) options.rotate = el.rotate + if (el.clip && el.clip.shape === 'ellipse') options.rounding = true + + pptxSlide.addImage(options) + } + else if (el.type === 'shape') { + if (el.special) { + const svgRef = document.querySelector(`#base-element-${el.id} svg`) as HTMLElement + const base64SVG = svg2Base64(svgRef) + + const options: pptxgen.ImageProps = { + data: base64SVG, + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + } + if (el.rotate) options.rotate = el.rotate + + pptxSlide.addImage(options) + } + else { + const scale = { + x: el.width / el.viewBox, + y: el.height / el.viewBox, + } + const points = formatPoints(toPoints(el.path), scale) + + const fillColor = formatColor(el.fill) + const opacity = el.opacity === undefined ? 1 : el.opacity + + const options: pptxgen.ShapeProps = { + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + fill: { color: fillColor.color, transparency: (1 - fillColor.alpha * opacity) * 100 }, + points, + } + if (el.flipH) options.flipH = el.flipH + if (el.flipV) options.flipV = el.flipV + if (el.outline?.width) { + options.line = { + color: formatColor(el.outline?.color || '#000000').color, + width: el.outline.width * 0.75, + dashType: el.outline.style === 'solid' ? 'solid' : 'dash', + } + } + pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options) + } + } + else if (el.type === 'line') { + let path = '' + const start = el.start.join(',') + const end = el.end.join(',') + if (el.broken) { + const mid = el.broken.join(',') + path = `M${start} L${mid} L${end}` + } + else if (el.curve) { + const mid = el.curve.join(',') + path = `M${start} Q${mid} ${end}` + } + else path = `M${start} L${end}` + + const points = formatPoints(toPoints(path)) + const { minX, maxX, minY, maxY } = getElementRange(el) + + const options: pptxgen.ShapeProps = { + x: el.left / 100, + y: el.top / 100, + w: (maxX - minX) / 100, + h: (maxY - minY) / 100, + line: { + color: formatColor(el.color).color, + width: el.width * 0.75, + dashType: el.style === 'solid' ? 'solid' : 'dash', + beginArrowType: el.points[0] ? 'arrow' : 'none', + endArrowType: el.points[1] ? 'arrow' : 'none', + }, + points, + } + pptxSlide.addShape('custGeom' as pptxgen.ShapeType, options) + } + else if (el.type === 'chart') { + const chartData = [] + for (let i = 0; i < el.data.series.length; i++) { + const item = el.data.series[i] + chartData.push({ + name: `系列${i + 1}`, + labels: el.data.labels, + values: item, + }) + } + + const chartColors = tinycolor(el.themeColor).analogous(10).map(item => item.toHexString()) + const options: pptxgen.IChartOpts = { + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + chartColors: el.chartType === 'pie' ? chartColors : chartColors.slice(0, el.data.series.length), + } + + let type = pptx.ChartType.bar + if (el.chartType === 'bar') { + type = pptx.ChartType.bar + options.barDir = el.options?.horizontalBars ? 'bar' : 'col' + } + else if (el.chartType === 'line') { + if (el.options?.showArea) type = pptx.ChartType.area + else if (el.options?.showLine === false) { + type = pptx.ChartType.scatter + + chartData.unshift({ name: 'X-Axis', values: Array(el.data.series[0].length).fill(0).map((v, i) => i) }) + options.lineSize = 0 + } + else type = pptx.ChartType.line + + if (el.options?.lineSmooth) options.lineSmooth = true + } + else if (el.chartType === 'pie') { + if (el.options?.donut) { + type = pptx.ChartType.doughnut + options.holeSize = 75 + } + else type = pptx.ChartType.pie + } + + pptxSlide.addChart(type, chartData, options) + } + else if (el.type === 'table') { + const hiddenCells = [] + for (let i = 0; i < el.data.length; i++) { + const rowData = el.data[i] + + for (let j = 0; j < rowData.length; j++) { + const cell = rowData[j] + if (cell.colspan > 1 || cell.rowspan > 1) { + for (let row = i; row < i + cell.rowspan; row++) { + for (let col = row === i ? j + 1 : j; col < j + cell.colspan; col++) hiddenCells.push(`${row}_${col}`) + } + } + } + } + + const tableData = [] + for (let i = 0; i < el.data.length; i++) { + const row = el.data[i] + const _row = [] + + for (let j = 0; j < row.length; j++) { + const cell = row[j] + const cellOptions: pptxgen.TableCellProps = { + colspan: cell.colspan, + rowspan: cell.rowspan, + bold: cell.style?.bold || false, + italic: cell.style?.em || false, + underline: { style: cell.style?.underline ? 'sng' : 'none' }, + align: cell.style?.align || 'left', + valign: 'middle', + fontFace: cell.style?.fontname || '微软雅黑', + fontSize: (cell.style?.fontsize ? parseInt(cell.style?.fontsize) : 14) * 0.75, + } + if (cell.style?.backcolor) { + const c = formatColor(cell.style.backcolor) + cellOptions.fill = { color: c.color, transparency: (1 - c.alpha) * 100 } + } + if (cell.style?.color) cellOptions.color = formatColor(cell.style.color).color + + if (!hiddenCells.includes(`${i}_${j}`)) { + _row.push({ + text: cell.text, + options: cellOptions, + }) + } + } + if (_row.length) tableData.push(_row) + } + + const options: pptxgen.TableProps = { + x: el.left / 100, + y: el.top / 100, + w: el.width / 100, + h: el.height / 100, + colW: el.colWidths.map(item => el.width * item / 100), + } + if (el.outline.width && el.outline.color) { + options.border = { + type: el.outline.style === 'solid' ? 'solid' : 'dash', + pt: el.outline.width * 0.75, + color: formatColor(el.outline.color).color, + } + } + + pptxSlide.addTable(tableData, options) + } + } + } + pptx.writeFile({ fileName: `pptist.pptx` }).then(() => exporting.value = false).catch(() => { + exporting.value = false + message.error('导出失败') + }) + } + + return { + exporting, + exportJSON, + exportPPTX, + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index cdaee133..f68d2fca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ import SvgWrapper from '@/components/SvgWrapper.vue' import CheckboxButton from '@/components/CheckboxButton.vue' import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue' import ColorPicker from '@/components/ColorPicker/index.vue' +import FullscreenSpin from '@/components/FullscreenSpin.vue' // antd 组件 import { @@ -53,6 +54,7 @@ app.component('SvgWrapper', SvgWrapper) app.component('CheckboxButton', CheckboxButton) app.component('CheckboxButtonGroup', CheckboxButtonGroup) app.component('ColorPicker', ColorPicker) +app.component('FullscreenSpin', FullscreenSpin) app.component('InputNumber', InputNumber) app.component('Divider', Divider) diff --git a/src/mocks/layout.ts b/src/mocks/layout.ts index ed97f930..ca225784 100644 --- a/src/mocks/layout.ts +++ b/src/mocks/layout.ts @@ -154,7 +154,7 @@ export const layouts: Slide[] = [ path: 'M 0 0 L 0 200 L 200 200 Z', fill: '#5b9bd5', fixedRatio: false, - flipH: true, + flipV: true, rotate: 0 }, { diff --git a/src/mocks/slides.ts b/src/mocks/slides.ts index 7c08fdac..c9d86dfd 100644 --- a/src/mocks/slides.ts +++ b/src/mocks/slides.ts @@ -29,7 +29,7 @@ export const slides: Slide[] = [ path: 'M 0 0 L 0 200 L 200 200 Z', fill: '#5b9bd5', fixedRatio: false, - flipH: true, + flipV: true, rotate: 0 }, { diff --git a/src/plugins/iconPark.ts b/src/plugins/iconPark.ts index 69e3a037..b8b78886 100644 --- a/src/plugins/iconPark.ts +++ b/src/plugins/iconPark.ts @@ -80,6 +80,7 @@ import { Logout, Erase, Clear, + FolderClose, } from '@icon-park/vue-next' export default { @@ -187,5 +188,6 @@ export default { app.component('IconArrowCircleLeft', ArrowCircleLeft) app.component('IconLogout', Logout) app.component('IconClear', Clear) + app.component('IconFolderClose', FolderClose) } } \ No newline at end of file diff --git a/src/types/slides.ts b/src/types/slides.ts index f51fee28..bcfe82b6 100644 --- a/src/types/slides.ts +++ b/src/types/slides.ts @@ -94,6 +94,7 @@ export interface PPTShapeElement extends PPTBaseElement { flipH?: boolean; flipV?: boolean; shadow?: PPTElementShadow; + special?: boolean; } export interface PPTLineElement extends Omit { @@ -133,7 +134,7 @@ export interface TableCellStyle { backcolor?: string; fontsize?: string; fontname?: string; - align?: string; + align?: 'left' | 'center' | 'right'; } export interface TableCell { id: string; diff --git a/src/utils/svg2Base64.ts b/src/utils/svg2Base64.ts new file mode 100644 index 00000000..5d222f7d --- /dev/null +++ b/src/utils/svg2Base64.ts @@ -0,0 +1,55 @@ +// https://github.com/scriptex/svg64 + +const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' +const PREFIX = 'data:image/svg+xml;base64,' + +const utf8Encode = (string: string) => { + string = string.replace(/\r\n/g, '\n') + let utftext = '' + + for (let n = 0; n < string.length; n++) { + const c = string.charCodeAt(n) + + if (c < 128) { + utftext += String.fromCharCode(c) + } + else if (c > 127 && c < 2048) { + utftext += String.fromCharCode((c >> 6) | 192) + utftext += String.fromCharCode((c & 63) | 128) + } + else { + utftext += String.fromCharCode((c >> 12) | 224) + utftext += String.fromCharCode(((c >> 6) & 63) | 128) + utftext += String.fromCharCode((c & 63) | 128) + } + } + + return utftext +} + +const encode = (input: string) => { + let output = '' + let chr1, chr2, chr3, enc1, enc2, enc3, enc4 + let i = 0 + input = utf8Encode(input) + while (i < input.length) { + chr1 = input.charCodeAt(i++) + chr2 = input.charCodeAt(i++) + chr3 = input.charCodeAt(i++) + enc1 = chr1 >> 2 + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4) + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6) + enc4 = chr3 & 63 + if (isNaN(chr2)) enc3 = enc4 = 64 + else if (isNaN(chr3)) enc4 = 64 + output = output + characters.charAt(enc1) + characters.charAt(enc2) + characters.charAt(enc3) + characters.charAt(enc4) + } + return output +} + +export const svg2Base64 = (element: Element) => { + const XMLS = new XMLSerializer() + const svg = XMLS.serializeToString(element) + + return PREFIX + encode(svg) +} \ No newline at end of file diff --git a/src/utils/svgPathParser.ts b/src/utils/svgPathParser.ts new file mode 100644 index 00000000..f5d7d2c7 --- /dev/null +++ b/src/utils/svgPathParser.ts @@ -0,0 +1,110 @@ +import { SVGPathData } from 'svg-pathdata' +import arcToBezier from 'svg-arc-to-cubic-bezier' + +const typeMap = { + 1: 'Z', + 2: 'M', + 4: 'H', + 8: 'V', + 16: 'L', + 32: 'C', + 64: 'S', + 128: 'Q', + 256: 'T', + 512: 'A', +} + +export const parseSvgPath = (d: string) => { + const pathData = new SVGPathData(d) + + const ret = pathData.commands.map(item => { + return { ...item, type: typeMap[item.type] } + }) + return ret +} + +export type SvgPath = ReturnType + +export const toPoints = (d: string) => { + const pathData = new SVGPathData(d) + + const points = [] + for (const item of pathData.commands) { + const type = typeMap[item.type] + + if (item.type === 2 || item.type === 16) { + points.push({ + x: item.x, + y: item.y, + relative: item.relative, + type, + }) + } + if (item.type === 32) { + points.push({ + x: item.x, + y: item.y, + curve: { + type: 'cubic', + x1: item.x1, + y1: item.y1, + x2: item.x2, + y2: item.y2, + }, + relative: item.relative, + type, + }) + } + else if (item.type === 128) { + points.push({ + x: item.x, + y: item.y, + curve: { + type: 'quadratic', + x1: item.x1, + y1: item.y1, + }, + relative: item.relative, + type, + }) + } + else if (item.type === 512) { + const lastPoint = points[points.length - 1] + if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue + + const cubicBezierPoints = arcToBezier({ + px: lastPoint.x as number, + py: lastPoint.y as number, + cx: item.x, + cy: item.y, + rx: item.rX, + ry: item.rY, + xAxisRotation: item.xRot, + largeArcFlag: item.lArcFlag, + sweepFlag: item.sweepFlag, + }) + for (const cbPoint of cubicBezierPoints) { + points.push({ + x: cbPoint.x, + y: cbPoint.y, + curve: { + type: 'cubic', + x1: cbPoint.x1, + y1: cbPoint.y1, + x2: cbPoint.x2, + y2: cbPoint.y2, + }, + relative: false, + type: 'C', + }) + } + } + else if (item.type === 1) { + points.push({ close: true, type }) + } + else continue + } + return points +} + +export type SvgPoints = ReturnType \ No newline at end of file diff --git a/src/views/Editor/EditorHeader/ExportDialog.vue b/src/views/Editor/EditorHeader/ExportDialog.vue deleted file mode 100644 index 89baa2eb..00000000 --- a/src/views/Editor/EditorHeader/ExportDialog.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/views/Editor/EditorHeader/index.vue b/src/views/Editor/EditorHeader/index.vue index 2cb41ed9..fc5e3c7f 100644 --- a/src/views/Editor/EditorHeader/index.vue +++ b/src/views/Editor/EditorHeader/index.vue @@ -1,6 +1,15 @@ @@ -74,15 +73,14 @@ import { MutationTypes, useStore } from '@/store' import useScreening from '@/hooks/useScreening' import useSlideHandler from '@/hooks/useSlideHandler' import useHistorySnapshot from '@/hooks/useHistorySnapshot' +import useExport from '@/hooks/useExport' import HotkeyDoc from './HotkeyDoc.vue' -import ExportDialog from './ExportDialog.vue' export default defineComponent({ name: 'editor-header', components: { HotkeyDoc, - ExportDialog, }, setup() { const store = useStore() @@ -90,6 +88,7 @@ export default defineComponent({ const { enterScreening, enterScreeningFromStart } = useScreening() const { createSlide, deleteSlide, resetSlides } = useSlideHandler() const { redo, undo } = useHistorySnapshot() + const { exporting, exportJSON, exportPPTX } = useExport() const showGridLines = computed(() => store.state.showGridLines) const toggleGridLines = () => { @@ -97,24 +96,25 @@ export default defineComponent({ } const hotkeyDrawerVisible = ref(false) - const exportDialogVisible = ref(false) const goIssues = () => { window.open('https://github.com/pipipi-pikachu/PPTist/issues') } return { + redo, + undo, + showGridLines, + hotkeyDrawerVisible, + exporting, enterScreening, enterScreeningFromStart, createSlide, deleteSlide, - redo, - undo, toggleGridLines, - showGridLines, resetSlides, - hotkeyDrawerVisible, - exportDialogVisible, + exportJSON, + exportPPTX, goIssues, } }, diff --git a/src/views/Editor/Toolbar/common/ElementFlip.vue b/src/views/Editor/Toolbar/common/ElementFlip.vue index 70cf27d9..9cae2358 100644 --- a/src/views/Editor/Toolbar/common/ElementFlip.vue +++ b/src/views/Editor/Toolbar/common/ElementFlip.vue @@ -3,13 +3,13 @@ 垂直翻转 水平翻转 diff --git a/src/views/components/ThumbnailSlide/ThumbnailElement.vue b/src/views/components/ThumbnailSlide/ThumbnailElement.vue index a012806b..85ab2f6c 100644 --- a/src/views/components/ThumbnailSlide/ThumbnailElement.vue +++ b/src/views/components/ThumbnailSlide/ThumbnailElement.vue @@ -1,6 +1,7 @@