mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 支持导入/导出特有文件格式
This commit is contained in:
parent
81c47ac08b
commit
afc86d0adf
72
src/hooks/useAddSlidesOrElements.ts
Normal file
72
src/hooks/useAddSlidesOrElements.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useSlidesStore, useMainStore } from '@/store'
|
||||
import { PPTElement, Slide } from '@/types/slides'
|
||||
import { createElementIdMap } from '@/utils/element'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
export default () => {
|
||||
const mainStore = useMainStore()
|
||||
const slidesStore = useSlidesStore()
|
||||
const { currentSlide } = storeToRefs(slidesStore)
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
/**
|
||||
* 添加指定的元素数据(一组)
|
||||
* @param elements 元素列表数据
|
||||
*/
|
||||
const addElementsFromData = (elements: PPTElement[]) => {
|
||||
const { groupIdMap, elIdMap } = createElementIdMap(elements)
|
||||
const currentSlideElementIdList = currentSlide.value.elements.map(el => el.id)
|
||||
|
||||
for (const element of elements) {
|
||||
const inCurrentSlide = currentSlideElementIdList.includes(element.id)
|
||||
|
||||
element.id = elIdMap[element.id]
|
||||
|
||||
if (inCurrentSlide) {
|
||||
element.left = element.left + 10
|
||||
element.top = element.top + 10
|
||||
}
|
||||
|
||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
||||
}
|
||||
slidesStore.addElement(elements)
|
||||
mainStore.setActiveElementIdList(Object.values(elIdMap))
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加指定的页面数据
|
||||
* @param slide 页面数据
|
||||
*/
|
||||
const addSlidesFromData = (slides: Slide[]) => {
|
||||
const newSlides = slides.map(slide => {
|
||||
const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
|
||||
|
||||
for (const element of slide.elements) {
|
||||
element.id = elIdMap[element.id]
|
||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
||||
}
|
||||
// 动画id替换
|
||||
if (slide.animations) {
|
||||
for (const animation of slide.animations) {
|
||||
animation.id = nanoid(10)
|
||||
animation.elId = elIdMap[animation.elId]
|
||||
}
|
||||
}
|
||||
return {
|
||||
...slide,
|
||||
id: nanoid(10),
|
||||
}
|
||||
})
|
||||
slidesStore.addSlide(newSlides)
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
return {
|
||||
addElementsFromData,
|
||||
addSlidesFromData,
|
||||
}
|
||||
}
|
@ -10,8 +10,10 @@ import { PPTElementOutline, PPTElementShadow, PPTElementLink, Slide } from '@/ty
|
||||
import { getElementRange, getLineElementPath, getTableSubThemeColor } from '@/utils/element'
|
||||
import { AST, toAST } from '@/utils/htmlParser'
|
||||
import { SvgPoints, toPoints } from '@/utils/svgPathParser'
|
||||
import { decrypt, encrypt } from '@/utils/crypto'
|
||||
import { svg2Base64 } from '@/utils/svg2Base64'
|
||||
import { message } from 'ant-design-vue'
|
||||
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
|
||||
|
||||
interface ExportImageConfig {
|
||||
quality: number;
|
||||
@ -22,6 +24,8 @@ interface ExportImageConfig {
|
||||
export default () => {
|
||||
const { slides, theme, viewportRatio } = storeToRefs(useSlidesStore())
|
||||
|
||||
const { addSlidesFromData } = useAddSlidesOrElements()
|
||||
|
||||
const exporting = ref(false)
|
||||
|
||||
// 导出图片
|
||||
@ -50,6 +54,29 @@ export default () => {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// 导出pptist文件(特有 .pptist 后缀文件)
|
||||
const exportSpecificFile = (_slides: Slide[]) => {
|
||||
const blob = new Blob([encrypt(JSON.stringify(_slides))], { type: '' })
|
||||
saveAs(blob, 'pptist_slides.pptist')
|
||||
}
|
||||
|
||||
// 导入pptist文件
|
||||
const importSpecificFile = (files: File[]) => {
|
||||
const file = files[0]
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', () => {
|
||||
try {
|
||||
const slides = JSON.parse(decrypt(reader.result as string))
|
||||
addSlidesFromData(slides)
|
||||
}
|
||||
catch {
|
||||
message.error('无法正确读取 / 解析该文件')
|
||||
}
|
||||
})
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 导出JSON文件
|
||||
const exportJSON = () => {
|
||||
const blob = new Blob([JSON.stringify(slides.value)], { type: '' })
|
||||
@ -738,6 +765,8 @@ export default () => {
|
||||
exporting,
|
||||
exportImage,
|
||||
exportJSON,
|
||||
importSpecificFile,
|
||||
exportSpecificFile,
|
||||
exportPPTX,
|
||||
}
|
||||
}
|
@ -1,12 +1,7 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useSlidesStore, useMainStore } from '@/store'
|
||||
import { pasteCustomClipboardString } from '@/utils/clipboard'
|
||||
import { PPTElement, Slide } from '@/types/slides'
|
||||
import { createElementIdMap } from '@/utils/element'
|
||||
import { parseText2Paragraphs } from '@/utils/textParser'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useCreateElement from '@/hooks/useCreateElement'
|
||||
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
|
||||
|
||||
interface PasteTextClipboardDataOptions {
|
||||
onlySlide?: boolean;
|
||||
@ -14,65 +9,8 @@ interface PasteTextClipboardDataOptions {
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const mainStore = useMainStore()
|
||||
const slidesStore = useSlidesStore()
|
||||
const { currentSlide } = storeToRefs(slidesStore)
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
const { createTextElement } = useCreateElement()
|
||||
|
||||
/**
|
||||
* 粘贴元素(一组)
|
||||
* @param elements 元素列表数据
|
||||
*/
|
||||
const addElementsFromClipboard = (elements: PPTElement[]) => {
|
||||
const { groupIdMap, elIdMap } = createElementIdMap(elements)
|
||||
const currentSlideElementIdList = currentSlide.value.elements.map(el => el.id)
|
||||
|
||||
for (const element of elements) {
|
||||
const inCurrentSlide = currentSlideElementIdList.includes(element.id)
|
||||
|
||||
element.id = elIdMap[element.id]
|
||||
|
||||
if (inCurrentSlide) {
|
||||
element.left = element.left + 10
|
||||
element.top = element.top + 10
|
||||
}
|
||||
|
||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
||||
}
|
||||
slidesStore.addElement(elements)
|
||||
mainStore.setActiveElementIdList(Object.values(elIdMap))
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* 粘贴页面
|
||||
* @param slide 页面数据
|
||||
*/
|
||||
const addSlidesFromClipboard = (slides: Slide[]) => {
|
||||
const newSlides = slides.map(slide => {
|
||||
const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
|
||||
|
||||
for (const element of slide.elements) {
|
||||
element.id = elIdMap[element.id]
|
||||
if (element.groupId) element.groupId = groupIdMap[element.groupId]
|
||||
}
|
||||
// 动画id替换
|
||||
if (slide.animations) {
|
||||
for (const animation of slide.animations) {
|
||||
animation.id = nanoid(10)
|
||||
animation.elId = elIdMap[animation.elId]
|
||||
}
|
||||
}
|
||||
return {
|
||||
...slide,
|
||||
id: nanoid(10),
|
||||
}
|
||||
})
|
||||
slidesStore.addSlide(newSlides)
|
||||
addHistorySnapshot()
|
||||
}
|
||||
const { addElementsFromData, addSlidesFromData } = useAddSlidesOrElements()
|
||||
|
||||
/**
|
||||
* 粘贴普通文本:创建为新的文本元素
|
||||
@ -102,8 +40,8 @@ export default () => {
|
||||
if (typeof clipboardData === 'object') {
|
||||
const { type, data } = clipboardData
|
||||
|
||||
if (type === 'elements' && !onlySlide) addElementsFromClipboard(data)
|
||||
else if (type === 'slides' && !onlyElements) addSlidesFromClipboard(data)
|
||||
if (type === 'elements' && !onlySlide) addElementsFromData(data)
|
||||
else if (type === 'slides' && !onlyElements) addSlidesFromData(data)
|
||||
}
|
||||
|
||||
// 普通文本
|
||||
@ -114,7 +52,6 @@ export default () => {
|
||||
}
|
||||
|
||||
return {
|
||||
addSlidesFromClipboard,
|
||||
pasteTextClipboardData,
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import { KEYS } from '@/configs/hotkey'
|
||||
import { message } from 'ant-design-vue'
|
||||
import usePasteTextClipboardData from '@/hooks/usePasteTextClipboardData'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useAddSlidesOrElements from '@/hooks//useAddSlidesOrElements'
|
||||
|
||||
export default () => {
|
||||
const mainStore = useMainStore()
|
||||
@ -21,7 +22,8 @@ export default () => {
|
||||
const selectedSlides = computed(() => slides.value.filter((item, index) => selectedSlidesIndex.value.includes(index)))
|
||||
const selectedSlidesId = computed(() => selectedSlides.value.map(item => item.id))
|
||||
|
||||
const { pasteTextClipboardData, addSlidesFromClipboard } = usePasteTextClipboardData()
|
||||
const { pasteTextClipboardData } = usePasteTextClipboardData()
|
||||
const { addSlidesFromData } = useAddSlidesOrElements()
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
// 重置幻灯片
|
||||
@ -108,7 +110,7 @@ export default () => {
|
||||
// 将当前页复制一份到下一页
|
||||
const copyAndPasteSlide = () => {
|
||||
const slide = JSON.parse(JSON.stringify(currentSlide.value))
|
||||
addSlidesFromClipboard([slide])
|
||||
addSlidesFromData([slide])
|
||||
}
|
||||
|
||||
// 删除当前页,若将删除全部页面,则执行重置幻灯片操作
|
||||
|
@ -1 +1 @@
|
||||
export type DialogForExportTypes = 'image' | 'pdf' | 'json' | 'pptx' | ''
|
||||
export type DialogForExportTypes = 'image' | 'pdf' | 'json' | 'pptx' | 'pptist' | ''
|
@ -5,6 +5,9 @@
|
||||
<div class="menu-item"><IconFolderClose /> <span class="text">文件</span></div>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<FileInput accept=".pptist" @change="files => importSpecificFile(files)">
|
||||
<MenuItem>导入 .pptist 文件</MenuItem>
|
||||
</FileInput>
|
||||
<MenuItem @click="setDialogForExport('pptx')">导出 PPTX</MenuItem>
|
||||
<MenuItem @click="setDialogForExport('image')">导出图片</MenuItem>
|
||||
<MenuItem @click="setDialogForExport('json')">导出 JSON</MenuItem>
|
||||
@ -80,6 +83,7 @@ import { useMainStore } 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'
|
||||
|
||||
@ -95,6 +99,7 @@ export default defineComponent({
|
||||
const { enterScreening, enterScreeningFromStart } = useScreening()
|
||||
const { createSlide, deleteSlide, resetSlides } = useSlideHandler()
|
||||
const { redo, undo } = useHistorySnapshot()
|
||||
const { importSpecificFile } = useExport()
|
||||
|
||||
const setDialogForExport = mainStore.setDialogForExport
|
||||
|
||||
@ -118,6 +123,7 @@ export default defineComponent({
|
||||
showGridLines,
|
||||
showRuler,
|
||||
hotkeyDrawerVisible,
|
||||
importSpecificFile,
|
||||
setDialogForExport,
|
||||
enterScreening,
|
||||
enterScreeningFromStart,
|
||||
|
@ -119,12 +119,6 @@ export default defineComponent({
|
||||
.config-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.8;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
width: 300px;
|
||||
|
137
src/views/Editor/ExportDialog/ExportSpecificFile.vue
Normal file
137
src/views/Editor/ExportDialog/ExportSpecificFile.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="export-pptist-dialog">
|
||||
<div class="configs">
|
||||
<div class="row">
|
||||
<div class="title">导出范围:</div>
|
||||
<RadioGroup
|
||||
class="config-item"
|
||||
v-model:value="rangeType"
|
||||
>
|
||||
<RadioButton style="width: 33.33%;" value="all">全部</RadioButton>
|
||||
<RadioButton style="width: 33.33%;" value="current">当前页</RadioButton>
|
||||
<RadioButton style="width: 33.33%;" value="custom">自定义</RadioButton>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div class="row" v-if="rangeType === 'custom'">
|
||||
<div class="title" :data-range="`(${range[0]} ~ ${range[1]})`">自定义范围:</div>
|
||||
<Slider
|
||||
class="config-item"
|
||||
range
|
||||
:min="1"
|
||||
:max="slides.length"
|
||||
:step="1"
|
||||
v-model:value="range"
|
||||
/>
|
||||
</div>
|
||||
<div class="tip">
|
||||
提示:.pptist 是本应用的特有文件后缀,支持将该类型的文件导入回应用中。
|
||||
</div>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<Button class="btn export" type="primary" @click="exportSpecificFile(selectedSlides)">导出 .pptist 文件</Button>
|
||||
<Button class="btn close" @click="close()">关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSlidesStore } from '@/store'
|
||||
import useExport from '@/hooks/useExport'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'export-pptist-dialog',
|
||||
setup(props, { emit }) {
|
||||
const { slides, currentSlide } = storeToRefs(useSlidesStore())
|
||||
|
||||
const rangeType = ref<'all' | 'current' | 'custom'>('all')
|
||||
const range = ref<[number, number]>([1, slides.value.length])
|
||||
|
||||
const selectedSlides = computed(() => {
|
||||
if (rangeType.value === 'all') return slides.value
|
||||
if (rangeType.value === 'current') return [currentSlide.value]
|
||||
return slides.value.filter((item, index) => {
|
||||
const [min, max] = range.value
|
||||
return index >= min - 1 && index <= max - 1
|
||||
})
|
||||
})
|
||||
|
||||
const close = () => emit('close')
|
||||
|
||||
const { exportSpecificFile } = useExport()
|
||||
|
||||
return {
|
||||
slides,
|
||||
rangeType,
|
||||
range,
|
||||
selectedSlides,
|
||||
exportSpecificFile,
|
||||
close,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.export-pptist-dialog {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.configs {
|
||||
width: 350px;
|
||||
height: calc(100% - 100px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100px;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: attr(data-range);
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.config-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
line-height: 1.8;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
width: 300px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.export {
|
||||
flex: 1;
|
||||
}
|
||||
.close {
|
||||
width: 100px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -25,6 +25,7 @@ import ExportImage from './ExportImage.vue'
|
||||
import ExportJSON from './ExportJSON.vue'
|
||||
import ExportPDF from './ExportPDF.vue'
|
||||
import ExportPPTX from './ExportPPTX.vue'
|
||||
import ExportSpecificFile from './ExportSpecificFile.vue'
|
||||
|
||||
interface TabItem {
|
||||
key: DialogForExportTypes;
|
||||
@ -42,6 +43,7 @@ export default defineComponent({
|
||||
const tabs: TabItem[] = [
|
||||
{ key: 'pptx', label: '导出 PPTX' },
|
||||
{ key: 'image', label: '导出图片' },
|
||||
{ key: 'pptist', label: '导出 .pptist' },
|
||||
{ key: 'json', label: '导出 JSON' },
|
||||
{ key: 'pdf', label: '打印 / 导出 PDF' },
|
||||
]
|
||||
@ -52,6 +54,7 @@ export default defineComponent({
|
||||
'json': ExportJSON,
|
||||
'pdf': ExportPDF,
|
||||
'pptx': ExportPPTX,
|
||||
'pptist': ExportSpecificFile,
|
||||
}
|
||||
return dialogMap[dialogForExport.value] || null
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user