diff --git a/.eslintrc.js b/.eslintrc.js index 9d47cc17..dac1e9e5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,7 +46,7 @@ module.exports = { }], 'default-case': 'error', 'consistent-this': ['error', '_this'], - 'max-depth': ['error', 6], + 'max-depth': ['error', 8], 'max-lines': ['error', 800], 'no-multi-str': 'error', 'space-infix-ops': 'error', diff --git a/package-lock.json b/package-lock.json index 42843d06..5b18655e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "nanoid": "^4.0.0", "pinia": "^2.0.32", "pptxgenjs": "^3.11.0", - "pptxtojson": "^0.0.8", + "pptxtojson": "^0.0.10", "prosemirror-commands": "^1.3.0", "prosemirror-dropcursor": "^1.6.0", "prosemirror-gapcursor": "^1.3.1", @@ -11735,9 +11735,9 @@ } }, "node_modules/pptxtojson": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.8.tgz", - "integrity": "sha512-9RDoPhTF9Nq7xlJbzmi05lfLV4TVa7sATX5uUmzWEj5mZyQ1SymIU8e6E1/h86/4BUVB0DbM9blwjCBXjiVlpw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.10.tgz", + "integrity": "sha512-OOVbl4y5tgnGkQIlSyZ742oBRivVSBYJV7dOwbhXEcnRZvPRVGBu8KPo7PE9QP2tDwpvuXJvqFrRaG7mbnfSfA==", "dependencies": { "jszip": "^3.10.1", "tinycolor2": "1.6.0", @@ -24676,9 +24676,9 @@ } }, "pptxtojson": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.8.tgz", - "integrity": "sha512-9RDoPhTF9Nq7xlJbzmi05lfLV4TVa7sATX5uUmzWEj5mZyQ1SymIU8e6E1/h86/4BUVB0DbM9blwjCBXjiVlpw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/pptxtojson/-/pptxtojson-0.0.10.tgz", + "integrity": "sha512-OOVbl4y5tgnGkQIlSyZ742oBRivVSBYJV7dOwbhXEcnRZvPRVGBu8KPo7PE9QP2tDwpvuXJvqFrRaG7mbnfSfA==", "requires": { "jszip": "^3.10.1", "tinycolor2": "1.6.0", diff --git a/package.json b/package.json index 20d59b35..333bf588 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "nanoid": "^4.0.0", "pinia": "^2.0.32", "pptxgenjs": "^3.11.0", - "pptxtojson": "^0.0.8", + "pptxtojson": "^0.0.10", "prosemirror-commands": "^1.3.0", "prosemirror-dropcursor": "^1.6.0", "prosemirror-gapcursor": "^1.3.1", diff --git a/src/assets/styles/antd.scss b/src/assets/styles/antd.scss index 10d00b42..c938d13b 100644 --- a/src/assets/styles/antd.scss +++ b/src/assets/styles/antd.scss @@ -52,6 +52,7 @@ // select .ant-select { user-select: none; + overflow: hidden; } .ant-select-item-option-active:not(.ant-select-item-option-disabled) { background-color: rgba($color: $themeColor, $alpha: .2); diff --git a/src/hooks/useImport.ts b/src/hooks/useImport.ts index af589777..78baf883 100644 --- a/src/hooks/useImport.ts +++ b/src/hooks/useImport.ts @@ -1,10 +1,12 @@ +import { ref } from 'vue' import { storeToRefs } from 'pinia' -import { parse } from 'pptxtojson' +import { parse, Shape, Element } from 'pptxtojson' 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 { decrypt } from '@/utils/crypto' import { ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes' +import { VIEWPORT_SIZE } from '@/configs/canvas' import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements' import { message } from 'ant-design-vue' @@ -15,6 +17,8 @@ export default () => { const { addSlidesFromData } = useAddSlidesOrElements() + const exporting = ref(false) + // 导入pptist文件 const importSpecificFile = (files: FileList, cover = false) => { const file = files[0] @@ -33,11 +37,47 @@ export default () => { 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文件 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) @@ -46,6 +86,10 @@ export default () => { const reader = new FileReader() reader.onload = async e => { const json = await parse(e.target!.result as ArrayBuffer) + + const width = json.size.width + const scale = VIEWPORT_SIZE / width + const slides: Slide[] = [] for (const item of json.slides) { const { type, value } = item.fill @@ -69,192 +113,227 @@ export default () => { elements: [], 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 = { - 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, - fixedRatio: false, - rotate: el.rotate, - outline: { - color: el.borderColor, - width: el.borderWidth, - style: el.borderType, - }, - text: { - content: el.content, + + const parseElements = (elements: Element[]) => { + for (const el of elements) { + el.width = el.width * scale + el.height = el.height * scale + el.left = el.left * scale + el.top = el.top * scale + + 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, - 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) } - } - - 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) - } - 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({ + else { + const shape = shapeList.find(item => item.pptxShapeType === el.shapType) + + const element: PPTShapeElement = { + type: 'shape', id: nanoid(10), - colspan: 1, - rowspan: cellData.rowSpan || 1, - text: cellData.text, - style, - }) + 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, + }, + 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) } - - 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: 2, - style: 'solid', - color: '#eeece1', - }, - theme: { - color: theme.value.themeColor, - rowHeader: true, - rowFooter: false, - colHeader: false, - colFooter: false, - }, - cellMinHeight: 36, - }) + 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), + colspan: 1, + rowspan: cellData.rowSpan || 1, + text: cellData.text, + style, + }) + } + 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: 2, + style: 'solid', + color: '#eeece1', + }, + theme: { + color: theme.value.themeColor, + rowHeader: true, + rowFooter: false, + colHeader: false, + 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) } addSlidesFromData(slides) + exporting.value = false } reader.readAsArrayBuffer(file) } @@ -262,5 +341,6 @@ export default () => { return { importSpecificFile, importPPTXFile, + exporting, } } \ No newline at end of file diff --git a/src/views/Editor/EditorHeader/index.vue b/src/views/Editor/EditorHeader/index.vue index f0b39530..3c673e6a 100644 --- a/src/views/Editor/EditorHeader/index.vue +++ b/src/views/Editor/EditorHeader/index.vue @@ -76,6 +76,8 @@ > + + @@ -90,6 +92,7 @@ import useImport from '@/hooks/useImport' import HotkeyDoc from './HotkeyDoc.vue' import FileInput from '@/components/FileInput.vue' +import FullscreenSpin from '@/components/FullscreenSpin.vue' import { Tooltip, Dropdown, @@ -104,7 +107,7 @@ const { gridLineSize, showRuler, showSelectPanel } = storeToRefs(mainStore) const { enterScreening, enterScreeningFromStart } = useScreening() const { createSlide, deleteSlide, resetSlides } = useSlideHandler() const { redo, undo } = useHistorySnapshot() -const { importSpecificFile, importPPTXFile } = useImport() +const { importSpecificFile, importPPTXFile, exporting } = useImport() const setDialogForExport = mainStore.setDialogForExport