feat: 支持导入/导出特有文件格式

This commit is contained in:
pipipi-pikachu 2022-05-29 10:53:05 +08:00
parent 81c47ac08b
commit afc86d0adf
9 changed files with 256 additions and 76 deletions

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

View File

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

View File

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

View File

@ -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])
}
// 删除当前页,若将删除全部页面,则执行重置幻灯片操作

View File

@ -1 +1 @@
export type DialogForExportTypes = 'image' | 'pdf' | 'json' | 'pptx' | ''
export type DialogForExportTypes = 'image' | 'pdf' | 'json' | 'pptx' | 'pptist' | ''

View File

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

View File

@ -119,12 +119,6 @@ export default defineComponent({
.config-item {
flex: 1;
}
.tip {
font-size: 12px;
color: #aaa;
line-height: 1.8;
}
}
.btns {
width: 300px;

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

View File

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