mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
485 lines
16 KiB
TypeScript
485 lines
16 KiB
TypeScript
import { ref } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { parse, type Shape, type Element, type ChartItem } from 'pptxtojson'
|
|
import { nanoid } from 'nanoid'
|
|
import { useSlidesStore } from '@/store'
|
|
import { decrypt } from '@/utils/crypto'
|
|
import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
|
|
import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
|
|
import useSlideHandler from '@/hooks/useSlideHandler'
|
|
import message from '@/utils/message'
|
|
import { getSvgPathRange } from '@/utils/svgPathParser'
|
|
import type {
|
|
Slide,
|
|
TableCellStyle,
|
|
TableCell,
|
|
ChartType,
|
|
SlideBackground,
|
|
PPTShapeElement,
|
|
PPTLineElement,
|
|
ShapeTextAlign,
|
|
PPTTextElement,
|
|
ChartOptions,
|
|
} from '@/types/slides'
|
|
|
|
const convertFontSizePtToPx = (html: string, ratio: number) => {
|
|
return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
|
|
return `font-size: ${(parseFloat(p1) * ratio).toFixed(1)}px`
|
|
})
|
|
}
|
|
|
|
export default () => {
|
|
const slidesStore = useSlidesStore()
|
|
const { theme } = storeToRefs(useSlidesStore())
|
|
|
|
const { addSlidesFromData } = useAddSlidesOrElements()
|
|
const { isEmptySlide } = useSlideHandler()
|
|
|
|
const exporting = ref(false)
|
|
|
|
// 导入pptist文件
|
|
const importSpecificFile = (files: FileList, cover = false) => {
|
|
const file = files[0]
|
|
|
|
const reader = new FileReader()
|
|
reader.addEventListener('load', () => {
|
|
try {
|
|
const slides = JSON.parse(decrypt(reader.result as string))
|
|
if (cover) {
|
|
slidesStore.updateSlideIndex(0)
|
|
slidesStore.setSlides(slides)
|
|
}
|
|
else if (isEmptySlide.value) slidesStore.setSlides(slides)
|
|
else addSlidesFromData(slides)
|
|
}
|
|
catch {
|
|
message.error('无法正确读取 / 解析该文件')
|
|
}
|
|
})
|
|
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 === 'solid' ? 'solid' : 'dashed',
|
|
color: el.borderColor,
|
|
points: ['', el.shapType === 'straightConnector1' ? 'arrow' : '']
|
|
}
|
|
}
|
|
|
|
// 导入PPTX文件
|
|
const importPPTXFile = (files: FileList) => {
|
|
const file = files[0]
|
|
if (!file) return
|
|
|
|
exporting.value = true
|
|
|
|
const shapeList: ShapePoolItem[] = []
|
|
for (const item of SHAPE_LIST) {
|
|
shapeList.push(...item.children)
|
|
}
|
|
|
|
const reader = new FileReader()
|
|
reader.onload = async e => {
|
|
const json = await parse(e.target!.result as ArrayBuffer)
|
|
|
|
const ratio = 96 / 72
|
|
const width = json.size.width
|
|
|
|
slidesStore.setViewportSize(width * ratio)
|
|
|
|
const slides: Slide[] = []
|
|
for (const item of json.slides) {
|
|
const { type, value } = item.fill
|
|
let background: SlideBackground
|
|
if (type === 'image') {
|
|
background = {
|
|
type: 'image',
|
|
image: {
|
|
src: value.picBase64,
|
|
size: 'cover',
|
|
},
|
|
}
|
|
}
|
|
else if (type === 'gradient') {
|
|
background = {
|
|
type: 'gradient',
|
|
gradient: {
|
|
type: 'linear',
|
|
colors: value.colors.map(item => ({
|
|
...item,
|
|
pos: parseInt(item.pos),
|
|
})),
|
|
rotate: value.rot,
|
|
},
|
|
}
|
|
}
|
|
else {
|
|
background = {
|
|
type: 'solid',
|
|
color: value,
|
|
}
|
|
}
|
|
|
|
const slide: Slide = {
|
|
id: nanoid(10),
|
|
elements: [],
|
|
background,
|
|
}
|
|
|
|
const parseElements = (elements: Element[]) => {
|
|
for (const el of elements) {
|
|
const originWidth = el.width || 1
|
|
const originHeight = el.height || 1
|
|
const originLeft = el.left
|
|
const originTop = el.top
|
|
|
|
el.width = el.width * ratio
|
|
el.height = el.height * ratio
|
|
el.left = el.left * ratio
|
|
el.top = el.top * ratio
|
|
|
|
if (el.type === 'text') {
|
|
const textEl: PPTTextElement = {
|
|
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: convertFontSizePtToPx(el.content, ratio),
|
|
lineHeight: 1,
|
|
outline: {
|
|
color: el.borderColor,
|
|
width: el.borderWidth,
|
|
style: el.borderType === 'solid' ? 'solid' : 'dashed',
|
|
},
|
|
fill: el.fillColor,
|
|
vertical: el.isVertical,
|
|
}
|
|
if (el.shadow) {
|
|
textEl.shadow = {
|
|
h: el.shadow.h * ratio,
|
|
v: el.shadow.v * ratio,
|
|
blur: el.shadow.blur * ratio,
|
|
color: el.shadow.color,
|
|
}
|
|
}
|
|
slide.elements.push(textEl)
|
|
}
|
|
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,
|
|
flipH: el.isFlipH,
|
|
flipV: el.isFlipV,
|
|
})
|
|
}
|
|
else if (el.type === 'audio') {
|
|
slide.elements.push({
|
|
type: 'audio',
|
|
id: nanoid(10),
|
|
src: el.blob,
|
|
width: el.width,
|
|
height: el.height,
|
|
left: el.left,
|
|
top: el.top,
|
|
rotate: 0,
|
|
fixedRatio: false,
|
|
color: theme.value.themeColor,
|
|
loop: false,
|
|
autoplay: false,
|
|
})
|
|
}
|
|
else if (el.type === 'video') {
|
|
slide.elements.push({
|
|
type: 'video',
|
|
id: nanoid(10),
|
|
src: (el.blob || el.src)!,
|
|
width: el.width,
|
|
height: el.height,
|
|
left: el.left,
|
|
top: el.top,
|
|
rotate: 0,
|
|
autoplay: false,
|
|
})
|
|
}
|
|
else if (el.type === 'shape') {
|
|
if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
|
|
const lineElement = parseLineElement(el)
|
|
slide.elements.push(lineElement)
|
|
}
|
|
else {
|
|
const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
|
|
|
|
const vAlignMap: { [key: string]: ShapeTextAlign } = {
|
|
'mid': 'middle',
|
|
'down': 'bottom',
|
|
'up': 'top',
|
|
}
|
|
|
|
const element: PPTShapeElement = {
|
|
type: 'shape',
|
|
id: nanoid(10),
|
|
width: el.width,
|
|
height: el.height,
|
|
left: el.left,
|
|
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 === 'solid' ? 'solid' : 'dashed',
|
|
},
|
|
text: {
|
|
content: convertFontSizePtToPx(el.content, ratio),
|
|
defaultFontName: theme.value.fontName,
|
|
defaultColor: theme.value.fontColor,
|
|
align: vAlignMap[el.vAlign] || 'middle',
|
|
},
|
|
flipH: el.isFlipH,
|
|
flipV: el.isFlipV,
|
|
}
|
|
if (el.shadow) {
|
|
element.shadow = {
|
|
h: el.shadow.h * ratio,
|
|
v: el.shadow.v * ratio,
|
|
blur: el.shadow.blur * ratio,
|
|
color: el.shadow.color,
|
|
}
|
|
}
|
|
|
|
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 && pathFormula.editable) {
|
|
element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
|
|
element.keypoints = pathFormula.defaultValue
|
|
}
|
|
else element.path = pathFormula.formula(el.width, el.height)
|
|
}
|
|
}
|
|
if (el.shapType === 'custom') {
|
|
if (el.path!.indexOf('NaN') !== -1) element.path = ''
|
|
else {
|
|
element.special = true
|
|
element.path = el.path!
|
|
|
|
const { maxX, maxY } = getSvgPathRange(element.path)
|
|
element.viewBox = [maxX || originWidth, maxY || originHeight]
|
|
}
|
|
}
|
|
|
|
if (element.path) 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]
|
|
|
|
let textDiv: HTMLDivElement | null = document.createElement('div')
|
|
textDiv.innerHTML = cellData.text
|
|
const p = textDiv.querySelector('p')
|
|
const align = p?.style.textAlign || 'left'
|
|
|
|
const span = textDiv.querySelector('span')
|
|
const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
|
|
const fontname = span?.style.fontFamily || ''
|
|
const color = span?.style.color || cellData.fontColor
|
|
|
|
rowCells.push({
|
|
id: nanoid(10),
|
|
colspan: cellData.colSpan || 1,
|
|
rowspan: cellData.rowSpan || 1,
|
|
text: textDiv.innerText,
|
|
style: {
|
|
...style,
|
|
align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
|
|
fontsize,
|
|
fontname,
|
|
color,
|
|
bold: cellData.fontBold,
|
|
backcolor: cellData.fillColor,
|
|
},
|
|
})
|
|
textDiv = null
|
|
}
|
|
data.push(rowCells)
|
|
}
|
|
|
|
const colWidths: number[] = new Array(col).fill(1 / col)
|
|
|
|
slide.elements.push({
|
|
type: 'table',
|
|
id: nanoid(10),
|
|
width: el.width,
|
|
height: el.height,
|
|
left: el.left,
|
|
top: el.top,
|
|
colWidths,
|
|
rotate: 0,
|
|
data,
|
|
outline: {
|
|
width: el.borderWidth || 2,
|
|
style: el.borderType === 'solid' ? 'solid' : 'dashed',
|
|
color: el.borderColor || '#eeece1',
|
|
},
|
|
cellMinHeight: 36,
|
|
})
|
|
}
|
|
else if (el.type === 'chart') {
|
|
let labels: string[]
|
|
let legends: string[]
|
|
let series: number[][]
|
|
|
|
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
|
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
|
|
legends = ['X', 'Y']
|
|
series = el.data
|
|
}
|
|
else {
|
|
const data = el.data as ChartItem[]
|
|
labels = Object.values(data[0].xlabels)
|
|
legends = data.map(item => item.key)
|
|
series = data.map(item => item.values.map(v => v.y))
|
|
}
|
|
|
|
const options: ChartOptions = {}
|
|
|
|
let chartType: ChartType = 'bar'
|
|
|
|
switch (el.chartType) {
|
|
case 'barChart':
|
|
case 'bar3DChart':
|
|
chartType = 'bar'
|
|
if (el.barDir === 'bar') chartType = 'column'
|
|
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
break
|
|
case 'lineChart':
|
|
case 'line3DChart':
|
|
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
chartType = 'line'
|
|
break
|
|
case 'areaChart':
|
|
case 'area3DChart':
|
|
if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
|
|
chartType = 'area'
|
|
break
|
|
case 'scatterChart':
|
|
case 'bubbleChart':
|
|
chartType = 'scatter'
|
|
break
|
|
case 'pieChart':
|
|
case 'pie3DChart':
|
|
chartType = 'pie'
|
|
break
|
|
case 'radarChart':
|
|
chartType = 'radar'
|
|
break
|
|
case 'doughnutChart':
|
|
chartType = 'ring'
|
|
break
|
|
default:
|
|
}
|
|
|
|
slide.elements.push({
|
|
type: 'chart',
|
|
id: nanoid(10),
|
|
chartType: chartType,
|
|
width: el.width,
|
|
height: el.height,
|
|
left: el.left,
|
|
top: el.top,
|
|
rotate: 0,
|
|
themeColors: [theme.value.themeColor],
|
|
textColor: theme.value.fontColor,
|
|
data: {
|
|
labels,
|
|
legends,
|
|
series,
|
|
},
|
|
options,
|
|
})
|
|
}
|
|
else if (el.type === 'group' || el.type === 'diagram') {
|
|
const elements = el.elements.map(_el => ({
|
|
..._el,
|
|
left: _el.left + originLeft,
|
|
top: _el.top + originTop,
|
|
}))
|
|
parseElements(elements)
|
|
}
|
|
}
|
|
}
|
|
parseElements(item.elements)
|
|
slides.push(slide)
|
|
}
|
|
slidesStore.updateSlideIndex(0)
|
|
slidesStore.setSlides(slides)
|
|
exporting.value = false
|
|
}
|
|
reader.readAsArrayBuffer(file)
|
|
}
|
|
|
|
return {
|
|
importSpecificFile,
|
|
importPPTXFile,
|
|
exporting,
|
|
}
|
|
} |