Merge branch 'master' of github.com:pipipi-pikachu/ppt_online_editor

This commit is contained in:
pipipi-pikachu 2021-04-09 12:44:34 +08:00
commit bc7e8bb956
21 changed files with 727 additions and 357 deletions

View File

@ -1,9 +1,7 @@
# 🎨 PPTist
> 一个基于 Vue3.x + TypeScript 的在线演示文稿应用还原了大部分PPT常用功能支持 文字、图片、形状、线条、图表、表格 6种最常用的元素类型每一种元素都拥有高度可编辑能力同时支持丰富的快捷键和右键菜单尽可能还原本地桌面应用的使用体验。
你可以对它进行二次开发,打造属于自己的 在线演示文稿应用 或者 在线设计工具(二次开发文档正在编写中)。
在线体验地址优先更新https://www.pptist.cn/
在线体验地址https://pipipi-pikachu.github.io/PPTist/
如果网络状态不佳可以访问国内镜像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
- [ ] 幻灯片备注
- [ ] 幻灯片模板
- [ ] 图表缩略图优化
- [ ] 公式元素
@ -179,3 +177,6 @@ A. 设置预置主题的作用是使新添加的元素和页面应用主题样
# 📄 开源协议
[MIT License](https://github.com/pipipi-pikachu/PPTist/blob/master/LICENSE)
# 💣 友情提示
本项目不接受任何形式的私人咨询,有任何问题欢迎在 github 提交你的 Issues

1
dist/css/app.abb643bc.css 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

7
dist/css/chunk-vendors.9281d9df.css 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

2
dist/index.html vendored
View File

@ -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

File diff suppressed because one or more lines are too long

1
dist/js/app.42e95de6.js.map 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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
View File

@ -2182,6 +2182,12 @@
"@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": {
"version": "7.1.3",
"resolved": "https://registry.npm.taobao.org/@types/glob/download/@types/glob-7.1.3.tgz",
@ -8072,6 +8078,11 @@
"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": {
"version": "1.0.0",
"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=",
"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": {
"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",

View File

@ -17,6 +17,8 @@
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"dexie": "^3.0.3",
"file-saver": "^2.0.5",
"html-to-image": "^1.3.25",
"lodash": "^4.17.20",
"mitt": "^2.1.0",
"prosemirror-commands": "^1.1.7",
@ -40,6 +42,7 @@
"@types/chartist": "^0.11.0",
"@types/clipboard": "^2.0.1",
"@types/crypto-js": "^4.0.1",
"@types/file-saver": "^2.0.1",
"@types/jest": "^24.0.19",
"@types/prosemirror-commands": "^1.0.3",
"@types/prosemirror-dropcursor": "^1.0.0",

View File

@ -39,6 +39,7 @@ import {
Menu,
Checkbox,
Drawer,
Spin,
} from 'ant-design-vue'
const app = createApp(App)
@ -75,6 +76,7 @@ app.component('Menu', Menu)
app.component('MenuItem', Menu.Item)
app.component('Checkbox', Checkbox)
app.component('Drawer', Drawer)
app.component('Spin', Spin)
app.use(store, key)
app.mount('#app')

View File

@ -205,6 +205,7 @@ export interface SlideBackground {
export interface Slide {
id: string;
elements: PPTElement[];
remark?: string;
background?: SlideBackground;
animations?: PPTAnimation[];
turningMode?: 'no' | 'fade' | 'slideX' | 'slideY';

View 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>

View File

@ -11,6 +11,7 @@
<MenuItem @click="deleteSlide()">删除页面</MenuItem>
<MenuItem @click="toggleGridLines()">{{ showGridLines ? '关闭网格线' : '打开网格线' }}</MenuItem>
<MenuItem @click="resetSlides()">重置幻灯片</MenuItem>
<MenuItem @click="exportDialogVisible = true">导出为</MenuItem>
</Menu>
</template>
</Dropdown>
@ -53,11 +54,22 @@
>
<HotkeyDoc />
</Drawer>
<Modal
v-model:visible="exportDialogVisible"
:footer="null"
centered
:closable="false"
:width="680"
destroyOnClose
>
<ExportDialog @close="exportDialogVisible = false"/>
</Modal>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
import { computed, defineAsyncComponent, defineComponent, ref } from 'vue'
import { MutationTypes, useStore } from '@/store'
import useScreening from '@/hooks/useScreening'
import useSlideHandler from '@/hooks/useSlideHandler'
@ -70,6 +82,7 @@ export default defineComponent({
name: 'editor-header',
components: {
HotkeyDoc,
ExportDialog: defineAsyncComponent(() => import('./ExportDialog.vue')),
},
setup() {
const store = useStore()
@ -88,6 +101,7 @@ export default defineComponent({
}
const hotkeyDrawerVisible = ref(false)
const exportDialogVisible = ref(false)
return {
enterScreening,
@ -101,6 +115,7 @@ export default defineComponent({
resetSlides,
openDoc,
hotkeyDrawerVisible,
exportDialogVisible,
}
},
})
@ -130,12 +145,12 @@ export default defineComponent({
transition: background-color .2s;
cursor: pointer;
&:hover {
background-color: $lightGray;
}
.text {
margin-left: 4px;
}
}
.left .menu-item:hover {
background-color: $lightGray;
}
</style>

View 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>

View File

@ -5,7 +5,12 @@
<Thumbnails class="layout-content-left" />
<div class="layout-content-center">
<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>
<Toolbar class="layout-content-right" />
</div>
@ -13,7 +18,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { defineComponent, ref } from 'vue'
import useGlobalHotkey from '@/hooks/useGlobalHotkey'
import usePasteEvent from '@/hooks/usePasteEvent'
@ -23,6 +28,7 @@ import Canvas from './Canvas/index.vue'
import CanvasTool from './CanvasTool/index.vue'
import Thumbnails from './Thumbnails/index.vue'
import Toolbar from './Toolbar/index.vue'
import Remark from './Remark/index.vue'
export default defineComponent({
name: 'editor',
@ -32,10 +38,17 @@ export default defineComponent({
CanvasTool,
Thumbnails,
Toolbar,
Remark,
},
setup() {
const remarkHeight = ref(40)
useGlobalHotkey()
usePasteEvent()
return {
remarkHeight,
}
},
})
</script>
@ -62,9 +75,6 @@ export default defineComponent({
.center-top {
height: 40px;
}
.center-body {
height: calc(100% - 40px);
}
}
.layout-content-right {
width: 260px;