mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
Merge branch 'master' of github.com:pipipi-pikachu/ppt_online_editor
This commit is contained in:
commit
bc7e8bb956
11
README.md
11
README.md
@ -1,9 +1,7 @@
|
|||||||
# 🎨 PPTist
|
# 🎨 PPTist
|
||||||
> 一个基于 Vue3.x + TypeScript 的在线演示文稿应用,还原了大部分PPT常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型,每一种元素都拥有高度可编辑能力,同时支持丰富的快捷键和右键菜单,尽可能还原本地桌面应用的使用体验。
|
> 一个基于 Vue3.x + TypeScript 的在线演示文稿应用,还原了大部分PPT常用功能,支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型,每一种元素都拥有高度可编辑能力,同时支持丰富的快捷键和右键菜单,尽可能还原本地桌面应用的使用体验。
|
||||||
|
|
||||||
你可以对它进行二次开发,打造属于自己的 在线演示文稿应用 或者 在线设计工具(二次开发文档正在编写中)。
|
在线体验地址:https://pipipi-pikachu.github.io/PPTist/
|
||||||
|
|
||||||
在线体验地址(优先更新):https://www.pptist.cn/
|
|
||||||
|
|
||||||
如果网络状态不佳,可以访问国内镜像:https://pptist.gitee.io/
|
如果网络状态不佳,可以访问国内镜像:https://pptist.gitee.io/
|
||||||
|
|
||||||
@ -25,7 +23,6 @@ npm run serve
|
|||||||
- 历史记录
|
- 历史记录
|
||||||
- 快捷键
|
- 快捷键
|
||||||
- 右键菜单
|
- 右键菜单
|
||||||
- 主题设置
|
|
||||||
|
|
||||||
## 幻灯片页面编辑
|
## 幻灯片页面编辑
|
||||||
- 页面添加、删除
|
- 页面添加、删除
|
||||||
@ -33,6 +30,8 @@ npm run serve
|
|||||||
- 页面复制粘贴
|
- 页面复制粘贴
|
||||||
- 背景设置(纯色、渐变、图片)
|
- 背景设置(纯色、渐变、图片)
|
||||||
- 网格线
|
- 网格线
|
||||||
|
- 主题设置
|
||||||
|
- 幻灯片备注
|
||||||
|
|
||||||
## 幻灯片元素编辑
|
## 幻灯片元素编辑
|
||||||
- 元素添加、删除
|
- 元素添加、删除
|
||||||
@ -100,7 +99,6 @@ npm run serve
|
|||||||
- 画笔工具
|
- 画笔工具
|
||||||
|
|
||||||
# 📃 TODO
|
# 📃 TODO
|
||||||
- [ ] 幻灯片备注
|
|
||||||
- [ ] 幻灯片模板
|
- [ ] 幻灯片模板
|
||||||
- [ ] 图表缩略图优化
|
- [ ] 图表缩略图优化
|
||||||
- [ ] 公式元素
|
- [ ] 公式元素
|
||||||
@ -179,3 +177,6 @@ A. 设置预置主题的作用是使新添加的元素和页面应用主题样
|
|||||||
|
|
||||||
# 📄 开源协议
|
# 📄 开源协议
|
||||||
[MIT License](https://github.com/pipipi-pikachu/PPTist/blob/master/LICENSE)
|
[MIT License](https://github.com/pipipi-pikachu/PPTist/blob/master/LICENSE)
|
||||||
|
|
||||||
|
# 💣 友情提示
|
||||||
|
本项目不接受任何形式的私人咨询,有任何问题欢迎在 github 提交你的 Issues
|
1
dist/css/app.abb643bc.css
vendored
Normal file
1
dist/css/app.abb643bc.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/css/app.c301a6c0.css
vendored
1
dist/css/app.c301a6c0.css
vendored
File diff suppressed because one or more lines are too long
7
dist/css/chunk-vendors.9281d9df.css
vendored
Normal file
7
dist/css/chunk-vendors.9281d9df.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
dist/css/chunk-vendors.9e3fd469.css
vendored
7
dist/css/chunk-vendors.9e3fd469.css
vendored
File diff suppressed because one or more lines are too long
2
dist/index.html
vendored
2
dist/index.html
vendored
@ -1 +1 @@
|
|||||||
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="renderer" content="webkit"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>PPTIST - 在线演示文稿</title><link href="css/app.c301a6c0.css" rel="preload" as="style"><link href="css/chunk-vendors.9e3fd469.css" rel="preload" as="style"><link href="js/app.89f18e38.js" rel="preload" as="script"><link href="js/chunk-vendors.01a8ee79.js" rel="preload" as="script"><link href="css/chunk-vendors.9e3fd469.css" rel="stylesheet"><link href="css/app.c301a6c0.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but pptist doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>document.oncontextmenu = e => e.preventDefault()</script><script src="js/chunk-vendors.01a8ee79.js"></script><script src="js/app.89f18e38.js"></script></body></html>
|
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="renderer" content="webkit"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>PPTIST - 在线演示文稿</title><link href="css/app.abb643bc.css" rel="preload" as="style"><link href="css/chunk-vendors.9281d9df.css" rel="preload" as="style"><link href="js/app.42e95de6.js" rel="preload" as="script"><link href="js/chunk-vendors.cdb8e65d.js" rel="preload" as="script"><link href="css/chunk-vendors.9281d9df.css" rel="stylesheet"><link href="css/app.abb643bc.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but pptist doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script>document.oncontextmenu = e => e.preventDefault()</script><script src="js/chunk-vendors.cdb8e65d.js"></script><script src="js/app.42e95de6.js"></script></body></html>
|
2
dist/js/app.42e95de6.js
vendored
Normal file
2
dist/js/app.42e95de6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/app.42e95de6.js.map
vendored
Normal file
1
dist/js/app.42e95de6.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/js/app.89f18e38.js
vendored
2
dist/js/app.89f18e38.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/app.89f18e38.js.map
vendored
1
dist/js/app.89f18e38.js.map
vendored
File diff suppressed because one or more lines are too long
328
dist/js/chunk-vendors.01a8ee79.js
vendored
328
dist/js/chunk-vendors.01a8ee79.js
vendored
File diff suppressed because one or more lines are too long
326
dist/js/chunk-vendors.cdb8e65d.js
vendored
Normal file
326
dist/js/chunk-vendors.cdb8e65d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
16
package-lock.json
generated
16
package-lock.json
generated
@ -2182,6 +2182,12 @@
|
|||||||
"@types/range-parser": "*"
|
"@types/range-parser": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/file-saver": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/file-saver/download/@types/file-saver-2.0.1.tgz?cache=0&sync_timestamp=1613380173874&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Ffile-saver%2Fdownload%2F%40types%2Ffile-saver-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-4Y64sGnkQve5VtMT9PrdPviHNU4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/glob": {
|
"@types/glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npm.taobao.org/@types/glob/download/@types/glob-7.1.3.tgz",
|
"resolved": "https://registry.npm.taobao.org/@types/glob/download/@types/glob-7.1.3.tgz",
|
||||||
@ -8072,6 +8078,11 @@
|
|||||||
"schema-utils": "^2.5.0"
|
"schema-utils": "^2.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/file-saver/download/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha1-1hz+LOBZ9BTYmendbUEH7iVnDDg="
|
||||||
|
},
|
||||||
"file-uri-to-path": {
|
"file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/file-uri-to-path/download/file-uri-to-path-1.0.0.tgz",
|
||||||
@ -9070,6 +9081,11 @@
|
|||||||
"integrity": "sha1-e15vfmZen7QfMAB+2eDUHpf7IUA=",
|
"integrity": "sha1-e15vfmZen7QfMAB+2eDUHpf7IUA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"html-to-image": {
|
||||||
|
"version": "1.3.25",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/html-to-image/download/html-to-image-1.3.25.tgz",
|
||||||
|
"integrity": "sha1-vxF4jQPFrT4FuPpO0AuhFkhGtFs="
|
||||||
|
},
|
||||||
"html-webpack-plugin": {
|
"html-webpack-plugin": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/html-webpack-plugin/download/html-webpack-plugin-3.2.0.tgz?cache=0&sync_timestamp=1615296080987&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhtml-webpack-plugin%2Fdownload%2Fhtml-webpack-plugin-3.2.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/html-webpack-plugin/download/html-webpack-plugin-3.2.0.tgz?cache=0&sync_timestamp=1615296080987&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhtml-webpack-plugin%2Fdownload%2Fhtml-webpack-plugin-3.2.0.tgz",
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"dexie": "^3.0.3",
|
"dexie": "^3.0.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"html-to-image": "^1.3.25",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"mitt": "^2.1.0",
|
"mitt": "^2.1.0",
|
||||||
"prosemirror-commands": "^1.1.7",
|
"prosemirror-commands": "^1.1.7",
|
||||||
@ -40,6 +42,7 @@
|
|||||||
"@types/chartist": "^0.11.0",
|
"@types/chartist": "^0.11.0",
|
||||||
"@types/clipboard": "^2.0.1",
|
"@types/clipboard": "^2.0.1",
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.0.1",
|
||||||
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jest": "^24.0.19",
|
"@types/jest": "^24.0.19",
|
||||||
"@types/prosemirror-commands": "^1.0.3",
|
"@types/prosemirror-commands": "^1.0.3",
|
||||||
"@types/prosemirror-dropcursor": "^1.0.0",
|
"@types/prosemirror-dropcursor": "^1.0.0",
|
||||||
|
@ -39,6 +39,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
Spin,
|
||||||
} from 'ant-design-vue'
|
} from 'ant-design-vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@ -75,6 +76,7 @@ app.component('Menu', Menu)
|
|||||||
app.component('MenuItem', Menu.Item)
|
app.component('MenuItem', Menu.Item)
|
||||||
app.component('Checkbox', Checkbox)
|
app.component('Checkbox', Checkbox)
|
||||||
app.component('Drawer', Drawer)
|
app.component('Drawer', Drawer)
|
||||||
|
app.component('Spin', Spin)
|
||||||
|
|
||||||
app.use(store, key)
|
app.use(store, key)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -205,6 +205,7 @@ export interface SlideBackground {
|
|||||||
export interface Slide {
|
export interface Slide {
|
||||||
id: string;
|
id: string;
|
||||||
elements: PPTElement[];
|
elements: PPTElement[];
|
||||||
|
remark?: string;
|
||||||
background?: SlideBackground;
|
background?: SlideBackground;
|
||||||
animations?: PPTAnimation[];
|
animations?: PPTAnimation[];
|
||||||
turningMode?: 'no' | 'fade' | 'slideX' | 'slideY';
|
turningMode?: 'no' | 'fade' | 'slideX' | 'slideY';
|
||||||
|
219
src/views/Editor/EditorHeader/ExportDialog.vue
Normal file
219
src/views/Editor/EditorHeader/ExportDialog.vue
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="export-dialog">
|
||||||
|
<div class="tabs">
|
||||||
|
<div
|
||||||
|
class="tab"
|
||||||
|
:class="{ 'active': tab.value === currentTab }"
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.value"
|
||||||
|
@click="currentTab = tab.value"
|
||||||
|
>{{tab.label}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content json" v-if="currentTab === 'json'">
|
||||||
|
<div class="json-preview">
|
||||||
|
<pre>{{slides}}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="json-configs">
|
||||||
|
<Button class="btn" type="primary" @click="exportJSON()">导出 JSON 文件</Button>
|
||||||
|
<Button class="btn" @click="emit('close')">关闭</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content image" v-if="currentTab === 'image'">
|
||||||
|
<div class="thumbnails-view">
|
||||||
|
<div class="thumbnails" ref="imageThumbnailsRef">
|
||||||
|
<ThumbnailSlide
|
||||||
|
class="thumbnail"
|
||||||
|
v-for="slide in slides"
|
||||||
|
:key="slide.id"
|
||||||
|
:slide="slide"
|
||||||
|
:size="1600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="configs">
|
||||||
|
<Button class="btn" type="primary" @click="exportImage('png')">导出 PNG 图片</Button>
|
||||||
|
<Button class="btn" type="primary" @click="exportImage('jpeg')">导出 JPEG 图片</Button>
|
||||||
|
<Button class="btn" @click="emit('close')">关闭</Button>
|
||||||
|
</div>
|
||||||
|
<div class="spinning" v-if="spinning">
|
||||||
|
<Spin />
|
||||||
|
<div class="tip">正在导出,请稍等...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from 'vue'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { toPng, toJpeg } from 'html-to-image'
|
||||||
|
|
||||||
|
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'export-dialog',
|
||||||
|
components: {
|
||||||
|
ThumbnailSlide,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useStore()
|
||||||
|
const slides = computed(() => store.state.slides)
|
||||||
|
|
||||||
|
const tabs = ref([
|
||||||
|
{ label: 'JSON', value: 'json' },
|
||||||
|
{ label: '图片', value: 'image' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentTab = ref('json')
|
||||||
|
const spinning = ref(false)
|
||||||
|
|
||||||
|
const exportJSON = () => {
|
||||||
|
const blob = new Blob([JSON.stringify(slides.value)], { type: '' })
|
||||||
|
saveAs(blob, 'pptist_slides.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageThumbnailsRef = ref<HTMLElement>()
|
||||||
|
const exportImage = (type: string) => {
|
||||||
|
spinning.value = true
|
||||||
|
const toImage = type === 'png' ? toPng : toJpeg
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!imageThumbnailsRef.value) return
|
||||||
|
|
||||||
|
toImage(imageThumbnailsRef.value, {
|
||||||
|
quality: 0.95,
|
||||||
|
width: 1600,
|
||||||
|
}).then(dataUrl => {
|
||||||
|
spinning.value = false
|
||||||
|
saveAs(dataUrl, `pptist_slides.${type}`)
|
||||||
|
}).catch(() => {
|
||||||
|
spinning.value = false
|
||||||
|
message.error('导出图片失败')
|
||||||
|
})
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
currentTab,
|
||||||
|
spinning,
|
||||||
|
slides,
|
||||||
|
exportJSON,
|
||||||
|
exportImage,
|
||||||
|
imageThumbnailsRef,
|
||||||
|
emit,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.export-dialog {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
height: 40px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
margin: -24px -24px 20px -24px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: $lightGray;
|
||||||
|
border-bottom: 1px solid $borderColor;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .tab {
|
||||||
|
border-left: 1px solid $borderColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.json-preview {
|
||||||
|
width: 460px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
margin-right: 20px;
|
||||||
|
background-color: #2d2d30;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.json-configs {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails-view {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.configs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 240px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinning {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: $themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -11,6 +11,7 @@
|
|||||||
<MenuItem @click="deleteSlide()">删除页面</MenuItem>
|
<MenuItem @click="deleteSlide()">删除页面</MenuItem>
|
||||||
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
|
||||||
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
|
||||||
|
<MenuItem @click="exportDialogVisible = true">导出为</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@ -53,11 +54,22 @@
|
|||||||
>
|
>
|
||||||
<HotkeyDoc />
|
<HotkeyDoc />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model:visible="exportDialogVisible"
|
||||||
|
:footer="null"
|
||||||
|
centered
|
||||||
|
:closable="false"
|
||||||
|
:width="680"
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<ExportDialog @close="exportDialogVisible = false"/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, ref } from 'vue'
|
import { computed, defineAsyncComponent, defineComponent, ref } from 'vue'
|
||||||
import { MutationTypes, useStore } from '@/store'
|
import { MutationTypes, useStore } from '@/store'
|
||||||
import useScreening from '@/hooks/useScreening'
|
import useScreening from '@/hooks/useScreening'
|
||||||
import useSlideHandler from '@/hooks/useSlideHandler'
|
import useSlideHandler from '@/hooks/useSlideHandler'
|
||||||
@ -70,6 +82,7 @@ export default defineComponent({
|
|||||||
name: 'editor-header',
|
name: 'editor-header',
|
||||||
components: {
|
components: {
|
||||||
HotkeyDoc,
|
HotkeyDoc,
|
||||||
|
ExportDialog: defineAsyncComponent(() => import('./ExportDialog.vue')),
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
@ -88,6 +101,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hotkeyDrawerVisible = ref(false)
|
const hotkeyDrawerVisible = ref(false)
|
||||||
|
const exportDialogVisible = ref(false)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enterScreening,
|
enterScreening,
|
||||||
@ -101,6 +115,7 @@ export default defineComponent({
|
|||||||
resetSlides,
|
resetSlides,
|
||||||
openDoc,
|
openDoc,
|
||||||
hotkeyDrawerVisible,
|
hotkeyDrawerVisible,
|
||||||
|
exportDialogVisible,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -130,12 +145,12 @@ export default defineComponent({
|
|||||||
transition: background-color .2s;
|
transition: background-color .2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $lightGray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left .menu-item:hover {
|
||||||
|
background-color: $lightGray;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
105
src/views/Editor/Remark/index.vue
Normal file
105
src/views/Editor/Remark/index.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div class="remark">
|
||||||
|
<div
|
||||||
|
class="resize-handler"
|
||||||
|
@mousedown="$event => resize($event)"
|
||||||
|
></div>
|
||||||
|
<textarea
|
||||||
|
:value="remark"
|
||||||
|
placeholder="点击输入演讲者备注"
|
||||||
|
@input="$event => handleInput($event)"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from 'vue'
|
||||||
|
import { MutationTypes, useStore } from '@/store'
|
||||||
|
import { Slide } from '@/types/slides'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'remark',
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useStore()
|
||||||
|
const currentSlide = computed<Slide>(() => store.getters.currentSlide)
|
||||||
|
const remark = computed(() => currentSlide.value?.remark || '')
|
||||||
|
|
||||||
|
const handleInput = (e: InputEvent) => {
|
||||||
|
const value = (e.target as HTMLTextAreaElement).value
|
||||||
|
store.commit(MutationTypes.UPDATE_SLIDE, { remark: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const resize = (e: MouseEvent) => {
|
||||||
|
let isMouseDown = true
|
||||||
|
const startPageY = e.pageY
|
||||||
|
const originHeight = props.height
|
||||||
|
|
||||||
|
document.onmousemove = e => {
|
||||||
|
if (!isMouseDown) return
|
||||||
|
|
||||||
|
const currentPageY = e.pageY
|
||||||
|
|
||||||
|
const moveY = currentPageY - startPageY
|
||||||
|
let newHeight = -moveY + originHeight
|
||||||
|
|
||||||
|
if (newHeight < 40) newHeight = 40
|
||||||
|
if (newHeight > 120) newHeight = 120
|
||||||
|
|
||||||
|
emit('update:height', newHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.onmouseup = () => {
|
||||||
|
isMouseDown = false
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remark,
|
||||||
|
handleInput,
|
||||||
|
resize,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.remark {
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid $borderColor;
|
||||||
|
background-color: $lightGray;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
resize: none;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.resize-handler {
|
||||||
|
height: 7px;
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: n-resize;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,7 +5,12 @@
|
|||||||
<Thumbnails class="layout-content-left" />
|
<Thumbnails class="layout-content-left" />
|
||||||
<div class="layout-content-center">
|
<div class="layout-content-center">
|
||||||
<CanvasTool class="center-top" />
|
<CanvasTool class="center-top" />
|
||||||
<Canvas class="center-body" />
|
<Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 40}px)` }" />
|
||||||
|
<Remark
|
||||||
|
class="center-bottom"
|
||||||
|
v-model:height="remarkHeight"
|
||||||
|
:style="{ height: `${remarkHeight}px` }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Toolbar class="layout-content-right" />
|
<Toolbar class="layout-content-right" />
|
||||||
</div>
|
</div>
|
||||||
@ -13,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent, ref } from 'vue'
|
||||||
|
|
||||||
import useGlobalHotkey from '@/hooks/useGlobalHotkey'
|
import useGlobalHotkey from '@/hooks/useGlobalHotkey'
|
||||||
import usePasteEvent from '@/hooks/usePasteEvent'
|
import usePasteEvent from '@/hooks/usePasteEvent'
|
||||||
@ -23,6 +28,7 @@ import Canvas from './Canvas/index.vue'
|
|||||||
import CanvasTool from './CanvasTool/index.vue'
|
import CanvasTool from './CanvasTool/index.vue'
|
||||||
import Thumbnails from './Thumbnails/index.vue'
|
import Thumbnails from './Thumbnails/index.vue'
|
||||||
import Toolbar from './Toolbar/index.vue'
|
import Toolbar from './Toolbar/index.vue'
|
||||||
|
import Remark from './Remark/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
@ -32,10 +38,17 @@ export default defineComponent({
|
|||||||
CanvasTool,
|
CanvasTool,
|
||||||
Thumbnails,
|
Thumbnails,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
|
Remark,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
|
const remarkHeight = ref(40)
|
||||||
|
|
||||||
useGlobalHotkey()
|
useGlobalHotkey()
|
||||||
usePasteEvent()
|
usePasteEvent()
|
||||||
|
|
||||||
|
return {
|
||||||
|
remarkHeight,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -62,9 +75,6 @@ export default defineComponent({
|
|||||||
.center-top {
|
.center-top {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
.center-body {
|
|
||||||
height: calc(100% - 40px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.layout-content-right {
|
.layout-content-right {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user