feat: 导入 PPTX Demo 补充

This commit is contained in:
pipipi-pikachu 2023-03-28 21:59:34 +08:00
parent 05f7edf852
commit 0b6c8c04b4
6 changed files with 271 additions and 187 deletions

View File

@ -46,7 +46,7 @@ module.exports = {
}], }],
'default-case': 'error', 'default-case': 'error',
'consistent-this': ['error', '_this'], 'consistent-this': ['error', '_this'],
'max-depth': ['error', 6], 'max-depth': ['error', 8],
'max-lines': ['error', 800], 'max-lines': ['error', 800],
'no-multi-str': 'error', 'no-multi-str': 'error',
'space-infix-ops': 'error', 'space-infix-ops': 'error',

14
package-lock.json generated
View File

@ -24,7 +24,7 @@
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"pinia": "^2.0.32", "pinia": "^2.0.32",
"pptxgenjs": "^3.11.0", "pptxgenjs": "^3.11.0",
"pptxtojson": "^0.0.8", "pptxtojson": "^0.0.10",
"prosemirror-commands": "^1.3.0", "prosemirror-commands": "^1.3.0",
"prosemirror-dropcursor": "^1.6.0", "prosemirror-dropcursor": "^1.6.0",
"prosemirror-gapcursor": "^1.3.1", "prosemirror-gapcursor": "^1.3.1",
@ -11735,9 +11735,9 @@
} }
}, },
"node_modules/pptxtojson": { "node_modules/pptxtojson": {
"version": "0.0.8", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.8.tgz", "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.10.tgz",
"integrity": "sha512-9RDoPhTF9Nq7xlJbzmi05lfLV4TVa7sATX5uUmzWEj5mZyQ1SymIU8e6E1/h86/4BUVB0DbM9blwjCBXjiVlpw==", "integrity": "sha512-OOVbl4y5tgnGkQIlSyZ742oBRivVSBYJV7dOwbhXEcnRZvPRVGBu8KPo7PE9QP2tDwpvuXJvqFrRaG7mbnfSfA==",
"dependencies": { "dependencies": {
"jszip": "^3.10.1", "jszip": "^3.10.1",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
@ -24676,9 +24676,9 @@
} }
}, },
"pptxtojson": { "pptxtojson": {
"version": "0.0.8", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.8.tgz", "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.10.tgz",
"integrity": "sha512-9RDoPhTF9Nq7xlJbzmi05lfLV4TVa7sATX5uUmzWEj5mZyQ1SymIU8e6E1/h86/4BUVB0DbM9blwjCBXjiVlpw==", "integrity": "sha512-OOVbl4y5tgnGkQIlSyZ742oBRivVSBYJV7dOwbhXEcnRZvPRVGBu8KPo7PE9QP2tDwpvuXJvqFrRaG7mbnfSfA==",
"requires": { "requires": {
"jszip": "^3.10.1", "jszip": "^3.10.1",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",

View File

@ -25,7 +25,7 @@
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"pinia": "^2.0.32", "pinia": "^2.0.32",
"pptxgenjs": "^3.11.0", "pptxgenjs": "^3.11.0",
"pptxtojson": "^0.0.8", "pptxtojson": "^0.0.10",
"prosemirror-commands": "^1.3.0", "prosemirror-commands": "^1.3.0",
"prosemirror-dropcursor": "^1.6.0", "prosemirror-dropcursor": "^1.6.0",
"prosemirror-gapcursor": "^1.3.1", "prosemirror-gapcursor": "^1.3.1",

View File

@ -52,6 +52,7 @@
// select // select
.ant-select { .ant-select {
user-select: none; user-select: none;
overflow: hidden;
} }
.ant-select-item-option-active:not(.ant-select-item-option-disabled) { .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
background-color: rgba($color: $themeColor, $alpha: .2); background-color: rgba($color: $themeColor, $alpha: .2);

View File

@ -1,10 +1,12 @@
import { ref } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { parse } from 'pptxtojson' import { parse, Shape, Element } from 'pptxtojson'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { Slide, TableCellStyle, TableCell, ChartType, ChartOptions, SlideBackground, PPTShapeElement } from '@/types/slides' import { Slide, TableCellStyle, TableCell, ChartType, ChartOptions, SlideBackground, PPTShapeElement, PPTLineElement } from '@/types/slides'
import { useSlidesStore } from '@/store' import { useSlidesStore } from '@/store'
import { decrypt } from '@/utils/crypto' import { decrypt } from '@/utils/crypto'
import { ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes' import { ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
import { VIEWPORT_SIZE } from '@/configs/canvas'
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements' import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -15,6 +17,8 @@ export default () => {
const { addSlidesFromData } = useAddSlidesOrElements() const { addSlidesFromData } = useAddSlidesOrElements()
const exporting = ref(false)
// 导入pptist文件 // 导入pptist文件
const importSpecificFile = (files: FileList, cover = false) => { const importSpecificFile = (files: FileList, cover = false) => {
const file = files[0] const file = files[0]
@ -33,11 +37,47 @@ export default () => {
reader.readAsText(file) reader.readAsText(file)
} }
const parseLineElement = (el: Shape): PPTLineElement => {
let start: [number, number] = [0, 0]
let end: [number, number] = [0, 0]
if (!el.isFlipV && !el.isFlipH) { // 右下
start = [0, 0]
end = [el.width, el.height]
}
else if (el.isFlipV && el.isFlipH) { // 左上
start = [el.width, el.height]
end = [0, 0]
}
else if (el.isFlipV && !el.isFlipH) { // 右上
start = [0, el.height]
end = [el.width, 0]
}
else { // 左下
start = [el.width, 0]
end = [0, el.height]
}
return {
type: 'line',
id: nanoid(10),
width: el.borderWidth || 1,
left: el.left,
top: el.top,
start,
end,
style: el.borderType,
color: el.borderColor,
points: ['', el.shapType === 'straightConnector1' ? 'arrow' : '']
}
}
// 导入PPTX文件 // 导入PPTX文件
const importPPTXFile = (files: FileList) => { const importPPTXFile = (files: FileList) => {
const file = files[0] const file = files[0]
if (!file) return if (!file) return
exporting.value = true
const shapeList: ShapePoolItem[] = [] const shapeList: ShapePoolItem[] = []
for (const item of SHAPE_LIST) { for (const item of SHAPE_LIST) {
shapeList.push(...item.children) shapeList.push(...item.children)
@ -46,6 +86,10 @@ export default () => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = async e => { reader.onload = async e => {
const json = await parse(e.target!.result as ArrayBuffer) const json = await parse(e.target!.result as ArrayBuffer)
const width = json.size.width
const scale = VIEWPORT_SIZE / width
const slides: Slide[] = [] const slides: Slide[] = []
for (const item of json.slides) { for (const item of json.slides) {
const { type, value } = item.fill const { type, value } = item.fill
@ -69,192 +113,227 @@ export default () => {
elements: [], elements: [],
background, background,
} }
for (const el of item.elements) {
if (el.type === 'text') {
slide.elements.push({
type: 'text',
id: nanoid(10),
width: el.width,
height: el.height,
left: el.left,
top: el.top,
rotate: el.rotate,
defaultFontName: theme.value.fontName,
defaultColor: theme.value.fontColor,
content: el.content,
lineHeight: 1,
outline: {
color: el.borderColor,
width: el.borderWidth,
style: el.borderType,
},
fill: el.fillColor,
})
}
else if (el.type === 'image') {
slide.elements.push({
type: 'image',
id: nanoid(10),
src: el.src,
width: el.width,
height: el.height,
left: el.left,
top: el.top,
fixedRatio: true,
rotate: el.rotate,
})
}
else if (el.type === 'shape') {
const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
const element: PPTShapeElement = { const parseElements = (elements: Element[]) => {
type: 'shape', for (const el of elements) {
id: nanoid(10), el.width = el.width * scale
width: el.width, el.height = el.height * scale
height: el.height, el.left = el.left * scale
left: el.left, el.top = el.top * scale
top: el.top,
viewBox: [200, 200], if (el.type === 'text') {
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z', slide.elements.push({
fill: el.fillColor, type: 'text',
fixedRatio: false, id: nanoid(10),
rotate: el.rotate, width: el.width,
outline: { height: el.height,
color: el.borderColor, left: el.left,
width: el.borderWidth, top: el.top,
style: el.borderType, rotate: el.rotate,
},
text: {
content: el.content,
defaultFontName: theme.value.fontName, defaultFontName: theme.value.fontName,
defaultColor: theme.value.fontColor, defaultColor: theme.value.fontColor,
align: 'middle', content: el.content,
lineHeight: 1,
outline: {
color: el.borderColor,
width: el.borderWidth,
style: el.borderType,
},
fill: el.fillColor,
})
}
else if (el.type === 'image') {
slide.elements.push({
type: 'image',
id: nanoid(10),
src: el.src,
width: el.width,
height: el.height,
left: el.left,
top: el.top,
fixedRatio: true,
rotate: el.rotate,
})
}
else if (el.type === 'shape') {
if (el.shapType === 'line' || el.shapType === 'straightConnector1') {
const lineElement = parseLineElement(el)
slide.elements.push(lineElement)
} }
} else {
const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
if (shape) { const element: PPTShapeElement = {
element.path = shape.path type: 'shape',
element.viewBox = shape.viewBox
if (shape.pathFormula) {
element.pathFormula = shape.pathFormula
element.viewBox = [el.width, el.height]
const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
if ('editable' in pathFormula) {
element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
element.keypoint = pathFormula.defaultValue
}
else element.path = pathFormula.formula(el.width, el.height)
}
}
slide.elements.push(element)
}
else if (el.type === 'table') {
const row = el.data.length
const col = el.data[0].length
const style: TableCellStyle = {
fontname: theme.value.fontName,
color: theme.value.fontColor,
}
const data: TableCell[][] = []
for (let i = 0; i < row; i++) {
const rowCells: TableCell[] = []
for (let j = 0; j < col; j++) {
const cellData = el.data[i][j]
rowCells.push({
id: nanoid(10), id: nanoid(10),
colspan: 1, width: el.width,
rowspan: cellData.rowSpan || 1, height: el.height,
text: cellData.text, left: el.left,
style, top: el.top,
}) viewBox: [200, 200],
path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
fill: el.fillColor || 'none',
fixedRatio: false,
rotate: el.rotate,
outline: {
color: el.borderColor,
width: el.borderWidth,
style: el.borderType,
},
text: {
content: el.content,
defaultFontName: theme.value.fontName,
defaultColor: theme.value.fontColor,
align: 'middle',
}
}
if (shape) {
element.path = shape.path
element.viewBox = shape.viewBox
if (shape.pathFormula) {
element.pathFormula = shape.pathFormula
element.viewBox = [el.width, el.height]
const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
if ('editable' in pathFormula) {
element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
element.keypoint = pathFormula.defaultValue
}
else element.path = pathFormula.formula(el.width, el.height)
}
}
slide.elements.push(element)
} }
data.push(rowCells)
} }
else if (el.type === 'table') {
const row = el.data.length
const col = el.data[0].length
const colWidths: number[] = new Array(col).fill(1 / col) const style: TableCellStyle = {
fontname: theme.value.fontName,
color: theme.value.fontColor,
}
const data: TableCell[][] = []
for (let i = 0; i < row; i++) {
const rowCells: TableCell[] = []
for (let j = 0; j < col; j++) {
const cellData = el.data[i][j]
rowCells.push({
id: nanoid(10),
colspan: 1,
rowspan: cellData.rowSpan || 1,
text: cellData.text,
style,
})
}
data.push(rowCells)
}
slide.elements.push({ const colWidths: number[] = new Array(col).fill(1 / col)
type: 'table',
id: nanoid(10), slide.elements.push({
width: el.width, type: 'table',
height: el.height, id: nanoid(10),
left: el.left, width: el.width,
top: el.top, height: el.height,
colWidths, left: el.left,
rotate: 0, top: el.top,
data, colWidths,
outline: { rotate: 0,
width: 2, data,
style: 'solid', outline: {
color: '#eeece1', width: 2,
}, style: 'solid',
theme: { color: '#eeece1',
color: theme.value.themeColor, },
rowHeader: true, theme: {
rowFooter: false, color: theme.value.themeColor,
colHeader: false, rowHeader: true,
colFooter: false, rowFooter: false,
}, colHeader: false,
cellMinHeight: 36, colFooter: false,
}) },
cellMinHeight: 36,
})
}
else if (el.type === 'chart') {
let labels: string[]
let legends: string[]
let series: number[][]
if (el.chartType === 'scatterChart') {
labels = el.data[0].map(item => item + '')
legends = ['系列1']
series = [el.data[1]]
}
else {
labels = Object.values(el.data[0].xlabels)
legends = el.data.map(item => item.key)
series = el.data.map(item => item.values.map(v => v.y))
}
let options: ChartOptions = {}
let chartType: ChartType = 'bar'
if (el.chartType === 'barChart') {
chartType = 'bar'
}
if (el.chartType === 'stackedBarChart') {
chartType = 'bar'
options = { stackBars: true }
}
else if (el.chartType === 'lineChart') {
chartType = 'line'
}
else if (el.chartType === 'areaChart') {
chartType = 'line'
options = { showArea: true }
}
else if (el.chartType === 'scatterChart') {
chartType = 'line'
options = { showLine: false }
}
else if (el.chartType === 'pieChart' || el.chartType === 'pie3DChart') {
chartType = 'pie'
}
slide.elements.push({
type: 'chart',
id: nanoid(10),
chartType: chartType,
width: el.width,
height: el.height,
left: el.left,
top: el.top,
rotate: 0,
themeColor: [theme.value.themeColor],
gridColor: theme.value.fontColor,
data: {
labels,
legends,
series,
},
options,
})
}
else if (el.type === 'group') {
const elements = el.elements.map(_el => ({
..._el,
left: _el.left + el.left,
top: _el.top + el.top,
}))
parseElements(elements)
}
} }
else if (el.type === 'chart') {
const labels = Object.values(el.data[0].xlabels)
const legends = el.data.map(item => item.key)
const series = el.data.map(item => item.values.map(v => v.y))
let options: ChartOptions = {}
let chartType: ChartType = 'bar'
if (el.chartType === 'barChart') {
chartType = 'bar'
}
if (el.chartType === 'stackedBarChart') {
chartType = 'bar'
options = { stackBars: true }
}
else if (el.chartType === 'lineChart') {
chartType = 'line'
}
else if (el.chartType === 'areaChart') {
chartType = 'line'
options = { showArea: true }
}
else if (el.chartType === 'scatterChart') {
chartType = 'line'
options = { showLine: false }
}
else if (el.chartType === 'pieChart' || el.chartType === 'pie3DChart') {
chartType = 'pie'
}
slide.elements.push({
type: 'chart',
id: nanoid(10),
chartType: chartType,
width: el.width,
height: el.height,
left: el.left,
top: el.top,
rotate: 0,
themeColor: [theme.value.themeColor],
gridColor: theme.value.fontColor,
data: {
labels,
legends,
series,
},
options,
})
}
// else if (el.type === 'group') {}
} }
parseElements(item.elements)
slides.push(slide) slides.push(slide)
} }
addSlidesFromData(slides) addSlidesFromData(slides)
exporting.value = false
} }
reader.readAsArrayBuffer(file) reader.readAsArrayBuffer(file)
} }
@ -262,5 +341,6 @@ export default () => {
return { return {
importSpecificFile, importSpecificFile,
importPPTXFile, importPPTXFile,
exporting,
} }
} }

View File

@ -76,6 +76,8 @@
> >
<HotkeyDoc /> <HotkeyDoc />
</Drawer> </Drawer>
<FullscreenSpin :loading="exporting" tip="正在导入..." />
</div> </div>
</template> </template>
@ -90,6 +92,7 @@ import useImport from '@/hooks/useImport'
import HotkeyDoc from './HotkeyDoc.vue' import HotkeyDoc from './HotkeyDoc.vue'
import FileInput from '@/components/FileInput.vue' import FileInput from '@/components/FileInput.vue'
import FullscreenSpin from '@/components/FullscreenSpin.vue'
import { import {
Tooltip, Tooltip,
Dropdown, Dropdown,
@ -104,7 +107,7 @@ const { gridLineSize, showRuler, showSelectPanel } = storeToRefs(mainStore)
const { enterScreening, enterScreeningFromStart } = useScreening() const { enterScreening, enterScreeningFromStart } = useScreening()
const { createSlide, deleteSlide, resetSlides } = useSlideHandler() const { createSlide, deleteSlide, resetSlides } = useSlideHandler()
const { redo, undo } = useHistorySnapshot() const { redo, undo } = useHistorySnapshot()
const { importSpecificFile, importPPTXFile } = useImport() const { importSpecificFile, importPPTXFile, exporting } = useImport()
const setDialogForExport = mainStore.setDialogForExport const setDialogForExport = mainStore.setDialogForExport