mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 导出PPTX文件
This commit is contained in:
parent
2391f56f20
commit
475fb495e6
29
README.md
29
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文件是非常重要的功能。但是经过本人一段时间的调研发现,该功能实现起来的复杂度远超过了预期。由于个人时间有限,暂时可能无法集中精力来做该功能,短时间内还是会更多优先去提升其他方面的使用体验。如果有做过相关内容的朋友,也欢迎给我提建议。
|
||||
|
||||
|
||||
# 💻 贡献代码
|
||||
首先感谢每一位关注本项目的朋友,由于本人时间精力有限,且目前项目规模不大,暂时没有团队化开发维护本项目的打算。但非常欢迎每一位对本项目感兴趣的朋友贡献代码。
|
||||
### 具体参考如下:
|
||||
|
130
package-lock.json
generated
130
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
34
src/components/FullscreenSpin.vue
Normal file
34
src/components/FullscreenSpin.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="fullscreen-spin" v-if="loading"><Spin :tip="tip" size="large" /></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'fullscreen-spin',
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tip: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fullscreen-spin {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba($color: #f1f1f1, $alpha: .7);
|
||||
}
|
||||
</style>
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
453
src/hooks/useExport.ts
Normal file
453
src/hooks/useExport.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -94,6 +94,7 @@ export interface PPTShapeElement extends PPTBaseElement {
|
||||
flipH?: boolean;
|
||||
flipV?: boolean;
|
||||
shadow?: PPTElementShadow;
|
||||
special?: boolean;
|
||||
}
|
||||
|
||||
export interface PPTLineElement extends Omit<PPTBaseElement, 'height'> {
|
||||
@ -133,7 +134,7 @@ export interface TableCellStyle {
|
||||
backcolor?: string;
|
||||
fontsize?: string;
|
||||
fontname?: string;
|
||||
align?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
export interface TableCell {
|
||||
id: string;
|
||||
|
55
src/utils/svg2Base64.ts
Normal file
55
src/utils/svg2Base64.ts
Normal file
@ -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)
|
||||
}
|
110
src/utils/svgPathParser.ts
Normal file
110
src/utils/svgPathParser.ts
Normal file
@ -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<typeof parseSvgPath>
|
||||
|
||||
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<typeof toPoints>
|
@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<div class="export-dialog">
|
||||
<div class="preview">
|
||||
<pre>{{slides}}</pre>
|
||||
</div>
|
||||
<div class="handle">
|
||||
<Button class="btn" type="primary" @click="exportJSON()">导出</Button>
|
||||
<Button class="btn" @click="emit('close')">关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useStore } from '@/store'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'export-dialog',
|
||||
setup(props, { emit }) {
|
||||
const store = useStore()
|
||||
const slides = computed(() => store.state.slides)
|
||||
|
||||
|
||||
const exportJSON = () => {
|
||||
const blob = new Blob([JSON.stringify(slides.value)], { type: '' })
|
||||
saveAs(blob, 'pptist_slides.json')
|
||||
}
|
||||
|
||||
return {
|
||||
slides,
|
||||
exportJSON,
|
||||
emit,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.export-dialog {
|
||||
height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.preview {
|
||||
width: 460px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
margin-right: 20px;
|
||||
background-color: #2d2d30;
|
||||
color: #fff;
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.handle {
|
||||
flex: 1;
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div class="editor-header">
|
||||
<div class="left">
|
||||
<Dropdown :trigger="['click']">
|
||||
<div class="menu-item"><IconFolderClose /> <span class="text">文件</span></div>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem @click="exportJSON()">导出 JSON</MenuItem>
|
||||
<MenuItem @click="exportPPTX()">导出 PPTX</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Dropdown :trigger="['click']">
|
||||
<div class="menu-item"><IconEdit /> <span class="text">编辑</span></div>
|
||||
<template #overlay>
|
||||
@ -11,7 +20,6 @@
|
||||
<MenuItem @click="deleteSlide()">删除页面</MenuItem>
|
||||
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
||||
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
||||
<MenuItem @click="exportDialogVisible = true">导出 JSON</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
@ -55,16 +63,7 @@
|
||||
<HotkeyDoc />
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
v-model:visible="exportDialogVisible"
|
||||
:footer="null"
|
||||
centered
|
||||
:closable="false"
|
||||
:width="680"
|
||||
destroyOnClose
|
||||
>
|
||||
<ExportDialog @close="exportDialogVisible = false"/>
|
||||
</Modal>
|
||||
<FullscreenSpin :loading="exporting" tip="正在导出..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -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,
|
||||
}
|
||||
},
|
||||
|
@ -3,13 +3,13 @@
|
||||
<CheckboxButtonGroup class="row">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="flipH"
|
||||
@click="updateFlip({ flipH: !flipH })"
|
||||
:checked="flipV"
|
||||
@click="updateFlip({ flipV: !flipV })"
|
||||
><IconFlipVertically /> 垂直翻转</CheckboxButton>
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="flipV"
|
||||
@click="updateFlip({ flipV: !flipV })"
|
||||
:checked="flipH"
|
||||
@click="updateFlip({ flipH: !flipH })"
|
||||
><IconFlipHorizontally /> 水平翻转</CheckboxButton>
|
||||
</CheckboxButtonGroup>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-element"
|
||||
:id="`base-element-${elementInfo.id}`"
|
||||
:style="{
|
||||
zIndex: elementIndex,
|
||||
}"
|
||||
|
@ -6,8 +6,8 @@ export default (flipH: Ref<boolean | undefined>, flipV: Ref<boolean | undefined>
|
||||
let style = ''
|
||||
|
||||
if (flipH.value && flipV.value) style = 'rotateX(180deg) rotateY(180deg)'
|
||||
else if (flipH.value) style = 'rotateX(180deg)'
|
||||
else if (flipV.value) style = 'rotateY(180deg)'
|
||||
else if (flipV.value) style = 'rotateX(180deg)'
|
||||
else if (flipH.value) style = 'rotateY(180deg)'
|
||||
|
||||
return style
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user