jvs-ppt-ui 0.0.11 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -1
- package/dist/lib/jvs-ppt-ui.es.js +21682 -21520
- package/dist/lib/jvs-ppt-ui.umd.js +85 -85
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/components/CanvasArea.vue +44 -21
- package/src/components/Header.vue +1592 -1564
- package/src/components/PptEditor.vue +5 -0
- package/src/components/PresentationMode.vue +2 -1
- package/src/components/SidebarLeft.vue +17 -5
- package/src/components/TemplateGallery.vue +217 -119
- package/src/components/canvas/elements/CanvasShapeElement.vue +3 -3
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -4
- package/src/composables/canvas/useCanvasShortcuts.ts +50 -1
- package/src/composables/canvas/useCanvasTable.ts +8 -3
- package/src/composables/canvas/useCanvasText.ts +7 -3
- package/src/composables/sidebar/useSidebarElementTransform.ts +17 -4
- package/src/composables/useSlides.ts +33 -14
- package/src/utils/groupFlip.ts +108 -0
|
@@ -13,10 +13,11 @@ import { parseColorString, extractTextAlignFromHtml, updateTextAlignInHtml, norm
|
|
|
13
13
|
|
|
14
14
|
import { shapeFormulas, shapeList as getPptxShapeList, ensurePptxShapes } from '../utils/pptxShapesLazy'
|
|
15
15
|
import { inferElementTextType, type SlideTextType } from '../utils/textTypes'
|
|
16
|
+
import { applyGroupGeometryFlip } from '../utils/groupFlip'
|
|
16
17
|
// 快捷键弹层异步化;mock DSL 模板(~77KB)改为触发时动态加载
|
|
17
18
|
const ShortcutsDialog = defineAsyncComponent(() => import('./ShortcutsDialog.vue'))
|
|
18
19
|
|
|
19
|
-
const { presentationTitle, exportSlides, importSlides, currentZipMeta, startPresentation, activeIndex, slides, notify, renderLatexToSvg, globalFontFamily, globalFontColor, globalThemeColor, themeSchemeFillMap, themeClrScheme, logicalWidth, logicalHeight, dslInputDialogVisible, aiPptDialogVisible, shortcutsDialogVisible, registerPPTParser, isMasterEditingView, enterMasterView, exitMasterView, zoomRatio, resetZoom, normalizeSlideBackground, getEffectiveMasterElementsForSlide, getEffectiveSlideBackground, uploadMediaResourcesInSlides, getCustomUploadHandler } = useSlides()
|
|
20
|
+
const { presentationTitle, exportSlides, importSlides, currentZipMeta, startPresentation, activeIndex, slides, notify, renderLatexToSvg, globalFontFamily, globalFontColor, globalThemeColor, themeSchemeFillMap, themeClrScheme, logicalWidth, logicalHeight, dslInputDialogVisible, aiPptDialogVisible, shortcutsDialogVisible, registerPPTParser, isMasterEditingView, enterMasterView, exitMasterView, zoomRatio, resetZoom, normalizeSlideBackground, getEffectiveMasterElementsForSlide, getEffectiveSlideBackground, uploadMediaResourcesInSlides, getCustomUploadHandler, isReadonly } = useSlides()
|
|
20
21
|
import { sanitizeNbspInHtml, checkIsVertical } from '../composables/useSlides'
|
|
21
22
|
|
|
22
23
|
// 快捷键弹层挂载闩锁:首次打开后保持挂载,异步 chunk 只加载一次
|
|
@@ -42,6 +43,7 @@ const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
|
42
43
|
const isEditingTitle = ref(false)
|
|
43
44
|
const titleInputRef = ref<HTMLInputElement | null>(null)
|
|
44
45
|
const startEditingTitle = () => {
|
|
46
|
+
if (isReadonly.value) return
|
|
45
47
|
isEditingTitle.value = true
|
|
46
48
|
nextTick(() => {
|
|
47
49
|
titleInputRef.value?.focus()
|
|
@@ -957,7 +959,7 @@ function parseHTMLTable(htmlStr: string, totalWidth?: number): { tableData: any[
|
|
|
957
959
|
const colgroupCols = tableEl.querySelectorAll('colgroup col')
|
|
958
960
|
const firstRowCells = tableEl.querySelectorAll('tr:first-child > td, tr:first-child > th')
|
|
959
961
|
const targetEls = colgroupCols.length >= totalCols ? colgroupCols : (firstRowCells.length > 0 ? firstRowCells : colgroupCols)
|
|
960
|
-
|
|
962
|
+
|
|
961
963
|
let colCounter = 0
|
|
962
964
|
targetEls.forEach((cEl) => {
|
|
963
965
|
if (colCounter >= totalCols) return
|
|
@@ -1785,960 +1787,960 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
1785
1787
|
|
|
1786
1788
|
// 2. Elements
|
|
1787
1789
|
const processElement = async (el: any, parentPixels: { left: number; top: number; width: number; height: number; rotate: number; flipH: boolean; flipV: boolean } | null = null) => {
|
|
1788
|
-
|
|
1790
|
+
if (el.visible === false) return
|
|
1789
1791
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
+
let pixelWidth = 0
|
|
1793
|
+
let pixelHeight = 0
|
|
1792
1794
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1795
|
+
const rawWidth = el.style?.width !== undefined ? el.style.width : el.width
|
|
1796
|
+
const rawHeight = el.style?.height !== undefined ? el.style.height : el.height
|
|
1797
|
+
const rawLeft = el.style?.left !== undefined ? el.style.left : el.left
|
|
1798
|
+
const rawTop = el.style?.top !== undefined ? el.style.top : el.top
|
|
1797
1799
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1800
|
+
if (parentPixels) {
|
|
1801
|
+
pixelWidth = parseValToPx(rawWidth, parentPixels.width, 100)
|
|
1802
|
+
pixelHeight = parseValToPx(rawHeight, parentPixels.height, 40)
|
|
1803
|
+
} else {
|
|
1804
|
+
pixelWidth = parseCoordinate(rawWidth, logicalWidth.value)
|
|
1805
|
+
pixelHeight = parseCoordinate(rawHeight, logicalHeight.value)
|
|
1806
|
+
}
|
|
1805
1807
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
}
|
|
1812
|
-
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1813
|
-
pixelWidth = domEl.offsetWidth
|
|
1814
|
-
}
|
|
1815
|
-
} else {
|
|
1816
|
-
const defaultFontSize = parseInt(el.style?.fontSize || '') || 18
|
|
1817
|
-
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1818
|
-
pixelWidth = 400
|
|
1819
|
-
}
|
|
1820
|
-
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1821
|
-
let topPadding = 4.8
|
|
1822
|
-
let bottomPadding = 4.8
|
|
1823
|
-
if (el.style?.padding) {
|
|
1824
|
-
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
1825
|
-
let parsedPadding = [4.8, 9.6, 4.8, 9.6]
|
|
1826
|
-
if (paddingParts.length === 4) {
|
|
1827
|
-
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
1828
|
-
} else if (paddingParts.length === 1) {
|
|
1829
|
-
const val = parseFloat(paddingParts[0]) || 0
|
|
1830
|
-
parsedPadding = [val, val, val, val]
|
|
1831
|
-
}
|
|
1832
|
-
topPadding = parsedPadding[0]
|
|
1833
|
-
bottomPadding = parsedPadding[2]
|
|
1834
|
-
}
|
|
1835
|
-
pixelHeight = estimateTextHeight(el.content || '', pixelWidth, defaultFontSize) + topPadding + bottomPadding
|
|
1836
|
-
}
|
|
1808
|
+
if (el.type === 'text') {
|
|
1809
|
+
const domEl = document.getElementById(`canvas-el-${el.id}`)
|
|
1810
|
+
if (domEl && domEl.offsetHeight > 0) {
|
|
1811
|
+
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1812
|
+
pixelHeight = domEl.offsetHeight
|
|
1837
1813
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const elRotate = typeof el.style?.rotate === 'number'
|
|
1841
|
-
? el.style.rotate
|
|
1842
|
-
: (parseFloat(el.style?.rotate || el.rotate || '0') || 0)
|
|
1843
|
-
const elFlipH = Boolean(el.style?.flipH || el.flipH || el.isFlipH)
|
|
1844
|
-
const elFlipV = Boolean(el.style?.flipV || el.flipV || el.isFlipV)
|
|
1845
|
-
|
|
1846
|
-
let pixelLeft = 0
|
|
1847
|
-
let pixelTop = 0
|
|
1848
|
-
let rotate = 0
|
|
1849
|
-
let flipH = elFlipH
|
|
1850
|
-
let flipV = elFlipV
|
|
1851
|
-
|
|
1852
|
-
if (parentPixels) {
|
|
1853
|
-
const rawChildLeft = parseValToPx(rawLeft, parentPixels.width, 0)
|
|
1854
|
-
const rawChildTop = parseValToPx(rawTop, parentPixels.height, 0)
|
|
1855
|
-
|
|
1856
|
-
const pW = parentPixels.width
|
|
1857
|
-
const pH = parentPixels.height
|
|
1858
|
-
const cCenterRelX = (rawChildLeft + pixelWidth / 2) - pW / 2
|
|
1859
|
-
const cCenterRelY = (rawChildTop + pixelHeight / 2) - pH / 2
|
|
1860
|
-
|
|
1861
|
-
let flippedRelX = parentPixels.flipH ? -cCenterRelX : cCenterRelX
|
|
1862
|
-
let flippedRelY = parentPixels.flipV ? -cCenterRelY : cCenterRelY
|
|
1863
|
-
|
|
1864
|
-
let rotRelX = flippedRelX
|
|
1865
|
-
let rotRelY = flippedRelY
|
|
1866
|
-
if (parentPixels.rotate) {
|
|
1867
|
-
const rad = (parentPixels.rotate * Math.PI) / 180
|
|
1868
|
-
rotRelX = flippedRelX * Math.cos(rad) - flippedRelY * Math.sin(rad)
|
|
1869
|
-
rotRelY = flippedRelX * Math.sin(rad) + flippedRelY * Math.cos(rad)
|
|
1814
|
+
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1815
|
+
pixelWidth = domEl.offsetWidth
|
|
1870
1816
|
}
|
|
1871
|
-
|
|
1872
|
-
const newCenterX = pW / 2 + rotRelX
|
|
1873
|
-
const newCenterY = pH / 2 + rotRelY
|
|
1874
|
-
|
|
1875
|
-
pixelLeft = parentPixels.left + (newCenterX - pixelWidth / 2)
|
|
1876
|
-
pixelTop = parentPixels.top + (newCenterY - pixelHeight / 2)
|
|
1877
|
-
|
|
1878
|
-
const parentOddFlips = (parentPixels.flipH !== parentPixels.flipV)
|
|
1879
|
-
const childEffectiveRot = parentOddFlips ? -elRotate : elRotate
|
|
1880
|
-
rotate = ((parentPixels.rotate + childEffectiveRot) % 360 + 360) % 360
|
|
1881
|
-
|
|
1882
|
-
flipH = parentPixels.flipH ? !elFlipH : elFlipH
|
|
1883
|
-
flipV = parentPixels.flipV ? !elFlipV : elFlipV
|
|
1884
1817
|
} else {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
1891
|
-
const curGroupPixels = {
|
|
1892
|
-
left: pixelLeft,
|
|
1893
|
-
top: pixelTop,
|
|
1894
|
-
width: pixelWidth,
|
|
1895
|
-
height: pixelHeight,
|
|
1896
|
-
rotate,
|
|
1897
|
-
flipH,
|
|
1898
|
-
flipV
|
|
1899
|
-
}
|
|
1900
|
-
for (const child of el.elements) {
|
|
1901
|
-
await processElement(child, curGroupPixels)
|
|
1902
|
-
}
|
|
1903
|
-
return
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
const x = (pixelLeft / logicalWidth.value) * baseWidth
|
|
1907
|
-
const y = (pixelTop / logicalHeight.value) * baseHeight
|
|
1908
|
-
const w = (pixelWidth / logicalWidth.value) * baseWidth
|
|
1909
|
-
const h = (pixelHeight / logicalHeight.value) * baseHeight
|
|
1910
|
-
|
|
1911
|
-
if (el.type === 'text') {
|
|
1912
|
-
if (el.latex) {
|
|
1913
|
-
const svgString = renderLatexToSvg(el.latex)
|
|
1914
|
-
const base64SVG = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgString)))
|
|
1915
|
-
pptxSlide.addImage({
|
|
1916
|
-
data: base64SVG,
|
|
1917
|
-
x, y, w, h,
|
|
1918
|
-
rotate,
|
|
1919
|
-
objectName: el.id
|
|
1920
|
-
})
|
|
1921
|
-
}
|
|
1922
|
-
else if ((el.content && (el.content.includes('<table') || el.content.includes('<table'))) || (el.data && Array.isArray(el.data) && el.data.length > 0)) {
|
|
1923
|
-
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
1924
|
-
? parseHTMLTable(el.content, w)
|
|
1925
|
-
: parseTableElementData(el, w)
|
|
1926
|
-
if (res.tableData && res.tableData.length > 0) {
|
|
1927
|
-
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
1928
|
-
if (res.colW && res.colW.length > 0) {
|
|
1929
|
-
tableOpts.colW = res.colW
|
|
1930
|
-
}
|
|
1931
|
-
pptxSlide.addTable(res.tableData, tableOpts)
|
|
1932
|
-
}
|
|
1818
|
+
const defaultFontSize = parseInt(el.style?.fontSize || '') || 18
|
|
1819
|
+
if (!pixelWidth || el.style?.width === 'auto') {
|
|
1820
|
+
pixelWidth = 400
|
|
1933
1821
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
const defaultFontFamily = (el.style?.fontFamily && el.style.fontFamily !== 'inherit' && el.style.fontFamily !== '默认字体')
|
|
1938
|
-
? el.style.fontFamily
|
|
1939
|
-
: (globalFontFamily.value && globalFontFamily.value !== '默认字体' ? globalFontFamily.value : 'SourceHanSansCN')
|
|
1940
|
-
|
|
1941
|
-
const isBold = el.style?.fontWeight === 'bold' || el.style?.fontWeight === '700' || (parseInt(el.style?.fontWeight as string) || 0) > 500
|
|
1942
|
-
const isItalic = (el.style as any)?.fontStyle === 'italic'
|
|
1943
|
-
const isUnderline = !!(el.style?.textDecoration && el.style.textDecoration.includes('underline'))
|
|
1944
|
-
const isStrike = !!(el.style?.textDecoration && el.style.textDecoration.includes('line-through'))
|
|
1945
|
-
|
|
1946
|
-
const hasFillOrBorder = (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') || (parseFloat(el.style?.borderWidth || '') || 0) > 0
|
|
1947
|
-
|
|
1948
|
-
const textShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow) || (!hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined)
|
|
1949
|
-
let parsedTextShadow: any = undefined
|
|
1950
|
-
if (textShadowObj) {
|
|
1951
|
-
parsedTextShadow = convertElShadowToPptx(textShadowObj, ratioPx2Pt)
|
|
1952
|
-
}
|
|
1953
|
-
if (!parsedTextShadow) {
|
|
1954
|
-
let textShadowStr = el.style?.boxShadow
|
|
1955
|
-
if (!textShadowStr && el.content) {
|
|
1956
|
-
const match = el.content.match(/(?:text-shadow)\s*:\s*([^;"]+)/i)
|
|
1957
|
-
if (match) {
|
|
1958
|
-
textShadowStr = match[1].trim()
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
if (textShadowStr) {
|
|
1962
|
-
parsedTextShadow = parseCssShadow(textShadowStr, ratioPx2Pt)
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const shapeShadowObj = hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined
|
|
1967
|
-
let parsedShapeShadow: any = undefined
|
|
1968
|
-
if (shapeShadowObj) {
|
|
1969
|
-
parsedShapeShadow = convertElShadowToPptx(shapeShadowObj, ratioPx2Pt)
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
const textRefl = (el as any).textReflection || (el.style as any)?.textReflection || (!hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined)
|
|
1973
|
-
let parsedTextReflection: any = undefined
|
|
1974
|
-
if (textRefl) {
|
|
1975
|
-
parsedTextReflection = convertElReflectionToPptx(textRefl, ratioPx2Pt)
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
const shapeRefl = hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined
|
|
1979
|
-
let parsedShapeReflection: any = undefined
|
|
1980
|
-
if (shapeRefl) {
|
|
1981
|
-
parsedShapeReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
const textProps = formatHTMLToPPTX(el.content || '', {
|
|
1985
|
-
fontSize: defaultFontSize,
|
|
1986
|
-
color: defaultColor,
|
|
1987
|
-
fontFamily: defaultFontFamily,
|
|
1988
|
-
bold: isBold,
|
|
1989
|
-
italic: isItalic,
|
|
1990
|
-
underline: isUnderline,
|
|
1991
|
-
strike: isStrike,
|
|
1992
|
-
align: el.style?.textAlign || 'left',
|
|
1993
|
-
shadow: parsedTextShadow,
|
|
1994
|
-
reflection: parsedTextReflection
|
|
1995
|
-
})
|
|
1996
|
-
|
|
1997
|
-
let finalLineHeight: number | undefined = undefined
|
|
1998
|
-
if (el.style?.lineHeight !== undefined) {
|
|
1999
|
-
const val = typeof el.style.lineHeight === 'number'
|
|
2000
|
-
? el.style.lineHeight
|
|
2001
|
-
: parseFloat(el.style.lineHeight as string || '')
|
|
2002
|
-
if (!isNaN(val)) {
|
|
2003
|
-
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
let textValign = 'top'
|
|
2007
|
-
const currentValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2008
|
-
if (currentValign === 'middle' || currentValign === 'mid') {
|
|
2009
|
-
textValign = 'middle'
|
|
2010
|
-
} else if (currentValign === 'bottom' || currentValign === 'down') {
|
|
2011
|
-
textValign = 'bottom'
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
let textMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
1822
|
+
if (!pixelHeight || el.style?.height === 'auto') {
|
|
1823
|
+
let topPadding = 4.8
|
|
1824
|
+
let bottomPadding = 4.8
|
|
2015
1825
|
if (el.style?.padding) {
|
|
2016
1826
|
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
2017
|
-
let parsedPadding = [
|
|
1827
|
+
let parsedPadding = [4.8, 9.6, 4.8, 9.6]
|
|
2018
1828
|
if (paddingParts.length === 4) {
|
|
2019
1829
|
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2020
1830
|
} else if (paddingParts.length === 1) {
|
|
2021
1831
|
const val = parseFloat(paddingParts[0]) || 0
|
|
2022
1832
|
parsedPadding = [val, val, val, val]
|
|
2023
1833
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
parsedPadding[1] / ratioPx2Pt, // right
|
|
2027
|
-
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2028
|
-
parsedPadding[0] / ratioPx2Pt // top
|
|
2029
|
-
]
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
const textOpts: any = {
|
|
2033
|
-
x, y, w, h,
|
|
2034
|
-
valign: textValign as any,
|
|
2035
|
-
align: (el.style?.textAlign || 'left') as any,
|
|
2036
|
-
margin: textMargin,
|
|
2037
|
-
rotate, flipH, flipV,
|
|
2038
|
-
objectName: el.id
|
|
2039
|
-
}
|
|
2040
|
-
if (finalLineHeight !== undefined) {
|
|
2041
|
-
textOpts.lineSpacingMultiple = finalLineHeight
|
|
1834
|
+
topPadding = parsedPadding[0]
|
|
1835
|
+
bottomPadding = parsedPadding[2]
|
|
2042
1836
|
}
|
|
1837
|
+
pixelHeight = estimateTextHeight(el.content || '', pixelWidth, defaultFontSize) + topPadding + bottomPadding
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
2043
1841
|
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
1842
|
+
const elRotate = typeof el.style?.rotate === 'number'
|
|
1843
|
+
? el.style.rotate
|
|
1844
|
+
: (parseFloat(el.style?.rotate || el.rotate || '0') || 0)
|
|
1845
|
+
const elFlipH = Boolean(el.style?.flipH || el.flipH || el.isFlipH)
|
|
1846
|
+
const elFlipV = Boolean(el.style?.flipV || el.flipV || el.isFlipV)
|
|
1847
|
+
|
|
1848
|
+
let pixelLeft = 0
|
|
1849
|
+
let pixelTop = 0
|
|
1850
|
+
let rotate = 0
|
|
1851
|
+
let flipH = elFlipH
|
|
1852
|
+
let flipV = elFlipV
|
|
1853
|
+
|
|
1854
|
+
if (parentPixels) {
|
|
1855
|
+
const rawChildLeft = parseValToPx(rawLeft, parentPixels.width, 0)
|
|
1856
|
+
const rawChildTop = parseValToPx(rawTop, parentPixels.height, 0)
|
|
1857
|
+
|
|
1858
|
+
const pW = parentPixels.width
|
|
1859
|
+
const pH = parentPixels.height
|
|
1860
|
+
const cCenterRelX = (rawChildLeft + pixelWidth / 2) - pW / 2
|
|
1861
|
+
const cCenterRelY = (rawChildTop + pixelHeight / 2) - pH / 2
|
|
1862
|
+
|
|
1863
|
+
let flippedRelX = parentPixels.flipH ? -cCenterRelX : cCenterRelX
|
|
1864
|
+
let flippedRelY = parentPixels.flipV ? -cCenterRelY : cCenterRelY
|
|
1865
|
+
|
|
1866
|
+
let rotRelX = flippedRelX
|
|
1867
|
+
let rotRelY = flippedRelY
|
|
1868
|
+
if (parentPixels.rotate) {
|
|
1869
|
+
const rad = (parentPixels.rotate * Math.PI) / 180
|
|
1870
|
+
rotRelX = flippedRelX * Math.cos(rad) - flippedRelY * Math.sin(rad)
|
|
1871
|
+
rotRelY = flippedRelX * Math.sin(rad) + flippedRelY * Math.cos(rad)
|
|
1872
|
+
}
|
|
2050
1873
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const fillParsed = parseColor(el.style.textBackground)
|
|
2054
|
-
textOpts.fill = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2055
|
-
}
|
|
1874
|
+
const newCenterX = pW / 2 + rotRelX
|
|
1875
|
+
const newCenterY = pH / 2 + rotRelY
|
|
2056
1876
|
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const textBorderColorStr = el.style?.borderColor
|
|
2060
|
-
if (textBorderWidth > 0 && textBorderColorStr && textBorderColorStr !== 'transparent' && textBorderColorStr !== 'none') {
|
|
2061
|
-
const borderParsed = parseColor(textBorderColorStr)
|
|
2062
|
-
textOpts.line = {
|
|
2063
|
-
color: borderParsed.color,
|
|
2064
|
-
width: textBorderWidth / ratioPx2Pt,
|
|
2065
|
-
dashType: el.style?.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
1877
|
+
pixelLeft = parentPixels.left + (newCenterX - pixelWidth / 2)
|
|
1878
|
+
pixelTop = parentPixels.top + (newCenterY - pixelHeight / 2)
|
|
2068
1879
|
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1880
|
+
const parentOddFlips = (parentPixels.flipH !== parentPixels.flipV)
|
|
1881
|
+
const childEffectiveRot = parentOddFlips ? -elRotate : elRotate
|
|
1882
|
+
rotate = ((parentPixels.rotate + childEffectiveRot) % 360 + 360) % 360
|
|
2072
1883
|
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
1884
|
+
flipH = parentPixels.flipH ? !elFlipH : elFlipH
|
|
1885
|
+
flipV = parentPixels.flipV ? !elFlipV : elFlipV
|
|
1886
|
+
} else {
|
|
1887
|
+
pixelLeft = parseCoordinate(rawLeft, logicalWidth.value)
|
|
1888
|
+
pixelTop = parseCoordinate(rawTop, logicalHeight.value)
|
|
1889
|
+
rotate = (elRotate % 360 + 360) % 360
|
|
1890
|
+
}
|
|
2081
1891
|
|
|
2082
|
-
|
|
2083
|
-
|
|
1892
|
+
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
1893
|
+
const curGroupPixels = {
|
|
1894
|
+
left: pixelLeft,
|
|
1895
|
+
top: pixelTop,
|
|
1896
|
+
width: pixelWidth,
|
|
1897
|
+
height: pixelHeight,
|
|
1898
|
+
rotate,
|
|
1899
|
+
flipH,
|
|
1900
|
+
flipV
|
|
2084
1901
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
1902
|
+
for (const child of el.elements) {
|
|
1903
|
+
await processElement(child, curGroupPixels)
|
|
1904
|
+
}
|
|
1905
|
+
return
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const x = (pixelLeft / logicalWidth.value) * baseWidth
|
|
1909
|
+
const y = (pixelTop / logicalHeight.value) * baseHeight
|
|
1910
|
+
const w = (pixelWidth / logicalWidth.value) * baseWidth
|
|
1911
|
+
const h = (pixelHeight / logicalHeight.value) * baseHeight
|
|
2090
1912
|
|
|
2091
|
-
|
|
1913
|
+
if (el.type === 'text') {
|
|
1914
|
+
if (el.latex) {
|
|
1915
|
+
const svgString = renderLatexToSvg(el.latex)
|
|
1916
|
+
const base64SVG = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgString)))
|
|
1917
|
+
pptxSlide.addImage({
|
|
1918
|
+
data: base64SVG,
|
|
2092
1919
|
x, y, w, h,
|
|
2093
|
-
rotate,
|
|
1920
|
+
rotate,
|
|
2094
1921
|
objectName: el.id
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
parsedImgShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2106
|
-
}
|
|
2107
|
-
if (parsedImgShadow) {
|
|
2108
|
-
imgOpts.shadow = parsedImgShadow
|
|
2109
|
-
}
|
|
2110
|
-
const imgRefl = el.reflection || (el.style && el.style.reflection)
|
|
2111
|
-
if (imgRefl) {
|
|
2112
|
-
const parsedReflection = convertElReflectionToPptx(imgRefl, ratioPx2Pt)
|
|
2113
|
-
if (parsedReflection) {
|
|
2114
|
-
imgOpts.reflection = parsedReflection
|
|
1922
|
+
})
|
|
1923
|
+
}
|
|
1924
|
+
else if ((el.content && (el.content.includes('<table') || el.content.includes('<table'))) || (el.data && Array.isArray(el.data) && el.data.length > 0)) {
|
|
1925
|
+
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
1926
|
+
? parseHTMLTable(el.content, w)
|
|
1927
|
+
: parseTableElementData(el, w)
|
|
1928
|
+
if (res.tableData && res.tableData.length > 0) {
|
|
1929
|
+
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
1930
|
+
if (res.colW && res.colW.length > 0) {
|
|
1931
|
+
tableOpts.colW = res.colW
|
|
2115
1932
|
}
|
|
1933
|
+
pptxSlide.addTable(res.tableData, tableOpts)
|
|
2116
1934
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
const defaultFontSize = Math.max(1, Math.round((parseInt(el.style?.fontSize || '') || 18) * fontSizeFactor))
|
|
1938
|
+
const defaultColor = el.style?.color || globalFontColor.value || '#475569'
|
|
1939
|
+
const defaultFontFamily = (el.style?.fontFamily && el.style.fontFamily !== 'inherit' && el.style.fontFamily !== '默认字体')
|
|
1940
|
+
? el.style.fontFamily
|
|
1941
|
+
: (globalFontFamily.value && globalFontFamily.value !== '默认字体' ? globalFontFamily.value : 'SourceHanSansCN')
|
|
1942
|
+
|
|
1943
|
+
const isBold = el.style?.fontWeight === 'bold' || el.style?.fontWeight === '700' || (parseInt(el.style?.fontWeight as string) || 0) > 500
|
|
1944
|
+
const isItalic = (el.style as any)?.fontStyle === 'italic'
|
|
1945
|
+
const isUnderline = !!(el.style?.textDecoration && el.style.textDecoration.includes('underline'))
|
|
1946
|
+
const isStrike = !!(el.style?.textDecoration && el.style.textDecoration.includes('line-through'))
|
|
1947
|
+
|
|
1948
|
+
const hasFillOrBorder = (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') || (parseFloat(el.style?.borderWidth || '') || 0) > 0
|
|
1949
|
+
|
|
1950
|
+
const textShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow) || (!hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined)
|
|
1951
|
+
let parsedTextShadow: any = undefined
|
|
1952
|
+
if (textShadowObj) {
|
|
1953
|
+
parsedTextShadow = convertElShadowToPptx(textShadowObj, ratioPx2Pt)
|
|
1954
|
+
}
|
|
1955
|
+
if (!parsedTextShadow) {
|
|
1956
|
+
let textShadowStr = el.style?.boxShadow
|
|
1957
|
+
if (!textShadowStr && el.content) {
|
|
1958
|
+
const match = el.content.match(/(?:text-shadow)\s*:\s*([^;"]+)/i)
|
|
1959
|
+
if (match) {
|
|
1960
|
+
textShadowStr = match[1].trim()
|
|
2139
1961
|
}
|
|
2140
1962
|
}
|
|
1963
|
+
if (textShadowStr) {
|
|
1964
|
+
parsedTextShadow = parseCssShadow(textShadowStr, ratioPx2Pt)
|
|
1965
|
+
}
|
|
2141
1966
|
}
|
|
2142
1967
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
// Set defaults based on shape types to match CanvasArea.vue styles
|
|
2149
|
-
let defaultFill = '#3b82f6'
|
|
2150
|
-
let defaultBorderColor = '#2563eb'
|
|
2151
|
-
let defaultBorderWidth = '2px'
|
|
2152
|
-
|
|
2153
|
-
if (el.content === 'triangle') {
|
|
2154
|
-
defaultFill = '#f59e0b'
|
|
2155
|
-
defaultBorderColor = '#d97706'
|
|
2156
|
-
defaultBorderWidth = '3px'
|
|
2157
|
-
} else if (el.content === 'circle') {
|
|
2158
|
-
defaultFill = '#3b82f6'
|
|
2159
|
-
defaultBorderColor = '#2563eb'
|
|
2160
|
-
defaultBorderWidth = '2px'
|
|
2161
|
-
} else if (typeof el.content === 'string' && (el.content.startsWith('M') || el.content.startsWith('m'))) {
|
|
2162
|
-
defaultFill = '#3b82f6'
|
|
2163
|
-
defaultBorderColor = '#2563eb'
|
|
2164
|
-
defaultBorderWidth = '2px'
|
|
2165
|
-
} else { // rect or default
|
|
2166
|
-
defaultFill = '#10b981'
|
|
2167
|
-
defaultBorderColor = '#059669'
|
|
2168
|
-
defaultBorderWidth = '2px'
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
const style: any = el.style || {}
|
|
2172
|
-
let shapeFillOpt: any = undefined
|
|
2173
|
-
|
|
2174
|
-
// Helper function to extract element gradient configuration for PPTX export
|
|
2175
|
-
const getElementGradientConfig = (item: any): any => {
|
|
2176
|
-
if (!item) return undefined
|
|
2177
|
-
let gradient = item.gradient || item.style?.gradient
|
|
2178
|
-
if (!gradient) {
|
|
2179
|
-
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2180
|
-
if (rawFill && typeof rawFill === 'object' && rawFill.type === 'gradient') {
|
|
2181
|
-
const val = rawFill.value || {}
|
|
2182
|
-
gradient = {
|
|
2183
|
-
path: val.path || 'line',
|
|
2184
|
-
type: val.path === 'line' || !val.path ? 'linear' : 'radial',
|
|
2185
|
-
rotate: val.rot !== undefined ? val.rot : (val.rotate !== undefined ? val.rotate : 90),
|
|
2186
|
-
colors: (val.colors || []).map((c: any) => ({
|
|
2187
|
-
color: c.color || c.value || '#3b82f6',
|
|
2188
|
-
pos: c.pos !== undefined ? (typeof c.pos === 'string' ? parseInt(c.pos) : c.pos) : 0
|
|
2189
|
-
}))
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
return gradient
|
|
1968
|
+
const shapeShadowObj = hasFillOrBorder ? (el.shadow || (el.style && (el.style as any).shadow)) : undefined
|
|
1969
|
+
let parsedShapeShadow: any = undefined
|
|
1970
|
+
if (shapeShadowObj) {
|
|
1971
|
+
parsedShapeShadow = convertElShadowToPptx(shapeShadowObj, ratioPx2Pt)
|
|
2194
1972
|
}
|
|
2195
1973
|
|
|
2196
|
-
const
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
if (rawFill && typeof rawFill === 'object' && (rawFill.type === 'image' || rawFill.type === 'pattern')) {
|
|
2201
|
-
return rawFill.value?.src || rawFill.value?.url || rawFill.value
|
|
2202
|
-
}
|
|
2203
|
-
return undefined
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
const patternUrl = getElementPatternUrl(el)
|
|
2207
|
-
const g = getElementGradientConfig(el)
|
|
2208
|
-
|
|
2209
|
-
// SVG 图片填充 PowerPoint 无法可靠渲染 a:blipFill,先按形状显示尺寸栅格化为 PNG
|
|
2210
|
-
// (画布上图案填充本身是 preserveAspectRatio="none" 拉伸铺满,与 a:stretch 语义一致)
|
|
2211
|
-
const rasterizeSvgFill = (svgUrl: string, widthPx: number, heightPx: number): Promise<string | undefined> => {
|
|
2212
|
-
return new Promise((resolve) => {
|
|
2213
|
-
const img = new Image()
|
|
2214
|
-
img.onload = () => {
|
|
2215
|
-
try {
|
|
2216
|
-
const canvas = document.createElement('canvas')
|
|
2217
|
-
canvas.width = Math.max(1, Math.min(4096, Math.round(widthPx)))
|
|
2218
|
-
canvas.height = Math.max(1, Math.min(4096, Math.round(heightPx)))
|
|
2219
|
-
const ctx = canvas.getContext('2d')
|
|
2220
|
-
if (!ctx) return resolve(undefined)
|
|
2221
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
2222
|
-
resolve(canvas.toDataURL('image/png'))
|
|
2223
|
-
} catch {
|
|
2224
|
-
resolve(undefined)
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
img.onerror = () => resolve(undefined)
|
|
2228
|
-
img.src = svgUrl
|
|
2229
|
-
})
|
|
1974
|
+
const textRefl = (el as any).textReflection || (el.style as any)?.textReflection || (!hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined)
|
|
1975
|
+
let parsedTextReflection: any = undefined
|
|
1976
|
+
if (textRefl) {
|
|
1977
|
+
parsedTextReflection = convertElReflectionToPptx(textRefl, ratioPx2Pt)
|
|
2230
1978
|
}
|
|
2231
1979
|
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
}
|
|
1980
|
+
const shapeRefl = hasFillOrBorder ? (el.reflection || (el.style && el.style.reflection)) : undefined
|
|
1981
|
+
let parsedShapeReflection: any = undefined
|
|
1982
|
+
if (shapeRefl) {
|
|
1983
|
+
parsedShapeReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
const textProps = formatHTMLToPPTX(el.content || '', {
|
|
1987
|
+
fontSize: defaultFontSize,
|
|
1988
|
+
color: defaultColor,
|
|
1989
|
+
fontFamily: defaultFontFamily,
|
|
1990
|
+
bold: isBold,
|
|
1991
|
+
italic: isItalic,
|
|
1992
|
+
underline: isUnderline,
|
|
1993
|
+
strike: isStrike,
|
|
1994
|
+
align: el.style?.textAlign || 'left',
|
|
1995
|
+
shadow: parsedTextShadow,
|
|
1996
|
+
reflection: parsedTextReflection
|
|
1997
|
+
})
|
|
1998
|
+
|
|
1999
|
+
let finalLineHeight: number | undefined = undefined
|
|
2000
|
+
if (el.style?.lineHeight !== undefined) {
|
|
2001
|
+
const val = typeof el.style.lineHeight === 'number'
|
|
2002
|
+
? el.style.lineHeight
|
|
2003
|
+
: parseFloat(el.style.lineHeight as string || '')
|
|
2004
|
+
if (!isNaN(val)) {
|
|
2005
|
+
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2259
2006
|
}
|
|
2260
|
-
return undefined
|
|
2261
2007
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2008
|
+
let textValign = 'top'
|
|
2009
|
+
const currentValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2010
|
+
if (currentValign === 'middle' || currentValign === 'mid') {
|
|
2011
|
+
textValign = 'middle'
|
|
2012
|
+
} else if (currentValign === 'bottom' || currentValign === 'down') {
|
|
2013
|
+
textValign = 'bottom'
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
let textMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
2017
|
+
if (el.style?.padding) {
|
|
2018
|
+
const paddingParts = el.style.padding.trim().split(/\s+/)
|
|
2019
|
+
let parsedPadding = [0, 0, 0, 0]
|
|
2020
|
+
if (paddingParts.length === 4) {
|
|
2021
|
+
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2022
|
+
} else if (paddingParts.length === 1) {
|
|
2023
|
+
const val = parseFloat(paddingParts[0]) || 0
|
|
2024
|
+
parsedPadding = [val, val, val, val]
|
|
2025
|
+
}
|
|
2026
|
+
textMargin = [
|
|
2027
|
+
parsedPadding[3] / ratioPx2Pt, // left
|
|
2028
|
+
parsedPadding[1] / ratioPx2Pt, // right
|
|
2029
|
+
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2030
|
+
parsedPadding[0] / ratioPx2Pt // top
|
|
2031
|
+
]
|
|
2270
2032
|
}
|
|
2271
2033
|
|
|
2272
|
-
const
|
|
2034
|
+
const textOpts: any = {
|
|
2273
2035
|
x, y, w, h,
|
|
2274
|
-
|
|
2036
|
+
valign: textValign as any,
|
|
2037
|
+
align: (el.style?.textAlign || 'left') as any,
|
|
2038
|
+
margin: textMargin,
|
|
2275
2039
|
rotate, flipH, flipV,
|
|
2276
2040
|
objectName: el.id
|
|
2277
2041
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const borderColorStr = style.borderColor
|
|
2281
|
-
if (borderWidth > 0 && borderColorStr && borderColorStr !== 'transparent' && borderColorStr !== 'none') {
|
|
2282
|
-
const borderParsed = parseColor(borderColorStr)
|
|
2283
|
-
shapeOpts.line = {
|
|
2284
|
-
color: borderParsed.color,
|
|
2285
|
-
width: borderWidth / ratioPx2Pt,
|
|
2286
|
-
dashType: style.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2287
|
-
}
|
|
2042
|
+
if (finalLineHeight !== undefined) {
|
|
2043
|
+
textOpts.lineSpacingMultiple = finalLineHeight
|
|
2288
2044
|
}
|
|
2289
2045
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
if (!parsedShadow && style.boxShadow) {
|
|
2293
|
-
parsedShadow = parseCssShadow(style.boxShadow, ratioPx2Pt)
|
|
2046
|
+
if (parsedShapeShadow) {
|
|
2047
|
+
textOpts.shadow = parsedShapeShadow
|
|
2294
2048
|
}
|
|
2295
|
-
if (
|
|
2296
|
-
|
|
2049
|
+
if (parsedShapeReflection) {
|
|
2050
|
+
textOpts.reflection = parsedShapeReflection
|
|
2297
2051
|
}
|
|
2298
2052
|
|
|
2299
|
-
|
|
2300
|
-
if (
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2303
|
-
|
|
2053
|
+
// Apply background color to text element
|
|
2054
|
+
if (el.style?.textBackground && el.style.textBackground !== 'transparent' && el.style.textBackground !== 'none') {
|
|
2055
|
+
const fillParsed = parseColor(el.style.textBackground)
|
|
2056
|
+
textOpts.fill = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Apply border line style to text element
|
|
2060
|
+
const textBorderWidth = parseFloat(el.style?.borderWidth || '') || 0
|
|
2061
|
+
const textBorderColorStr = el.style?.borderColor
|
|
2062
|
+
if (textBorderWidth > 0 && textBorderColorStr && textBorderColorStr !== 'transparent' && textBorderColorStr !== 'none') {
|
|
2063
|
+
const borderParsed = parseColor(textBorderColorStr)
|
|
2064
|
+
textOpts.line = {
|
|
2065
|
+
color: borderParsed.color,
|
|
2066
|
+
width: textBorderWidth / ratioPx2Pt,
|
|
2067
|
+
dashType: el.style?.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2304
2068
|
}
|
|
2305
2069
|
}
|
|
2306
2070
|
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2071
|
+
if ((el.style as any)?.whiteSpace === 'nowrap') {
|
|
2072
|
+
textOpts.wrap = false
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const elAny = el as any
|
|
2076
|
+
if (elAny.autoFit) {
|
|
2077
|
+
if (elAny.autoFit.type === 'shape') {
|
|
2078
|
+
textOpts.fit = 'resize'
|
|
2079
|
+
} else if (elAny.autoFit.type === 'text') {
|
|
2080
|
+
textOpts.fit = 'shrink'
|
|
2314
2081
|
}
|
|
2315
|
-
shapeOpts.points = formatPoints(rawPoints, scale, pixelToInch) as any
|
|
2316
2082
|
}
|
|
2317
2083
|
|
|
2318
|
-
|
|
2084
|
+
pptxSlide.addText(textProps, textOpts)
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
else if (el.type === 'image') {
|
|
2088
|
+
let imgPath = await resolveMediaSrc(el.content || (el as any).src || '', 'image')
|
|
2089
|
+
if (!imgPath) {
|
|
2090
|
+
imgPath = createFallbackImageBase64('图片缺失')
|
|
2091
|
+
}
|
|
2319
2092
|
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2093
|
+
const imgOpts: pptxgen.ImageProps = {
|
|
2094
|
+
x, y, w, h,
|
|
2095
|
+
rotate, flipH, flipV,
|
|
2096
|
+
objectName: el.id
|
|
2097
|
+
}
|
|
2098
|
+
if (imgPath.startsWith('data:')) {
|
|
2099
|
+
imgOpts.data = imgPath
|
|
2100
|
+
} else {
|
|
2101
|
+
imgOpts.path = imgPath
|
|
2102
|
+
}
|
|
2325
2103
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2104
|
+
const imgShadow = el.shadow || (el.style && (el.style as any).shadow)
|
|
2105
|
+
let parsedImgShadow = convertElShadowToPptx(imgShadow, ratioPx2Pt)
|
|
2106
|
+
if (!parsedImgShadow && el.style?.boxShadow) {
|
|
2107
|
+
parsedImgShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2108
|
+
}
|
|
2109
|
+
if (parsedImgShadow) {
|
|
2110
|
+
imgOpts.shadow = parsedImgShadow
|
|
2111
|
+
}
|
|
2112
|
+
const imgRefl = el.reflection || (el.style && el.style.reflection)
|
|
2113
|
+
if (imgRefl) {
|
|
2114
|
+
const parsedReflection = convertElReflectionToPptx(imgRefl, ratioPx2Pt)
|
|
2115
|
+
if (parsedReflection) {
|
|
2116
|
+
imgOpts.reflection = parsedReflection
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2330
2119
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2120
|
+
if (el.clip && Array.isArray(el.clip.range) && el.clip.range.length >= 2) {
|
|
2121
|
+
const [start, end] = el.clip.range
|
|
2122
|
+
if (Array.isArray(start) && start.length >= 2 && Array.isArray(end) && end.length >= 2) {
|
|
2123
|
+
const [startX, startY] = start
|
|
2124
|
+
const [endX, endY] = end
|
|
2125
|
+
|
|
2126
|
+
const fractionX = (endX - startX) / 100
|
|
2127
|
+
const fractionY = (endY - startY) / 100
|
|
2128
|
+
if (fractionX > 0 && fractionY > 0) {
|
|
2129
|
+
const originW = w / fractionX
|
|
2130
|
+
const originH = h / fractionY
|
|
2131
|
+
|
|
2132
|
+
imgOpts.w = originW
|
|
2133
|
+
imgOpts.h = originH
|
|
2134
|
+
imgOpts.sizing = {
|
|
2135
|
+
type: 'crop',
|
|
2136
|
+
x: (startX / 100) * originW,
|
|
2137
|
+
y: (startY / 100) * originH,
|
|
2138
|
+
w: w,
|
|
2139
|
+
h: h,
|
|
2340
2140
|
}
|
|
2341
|
-
shapeMargin = [
|
|
2342
|
-
parsedPadding[3] / ratioPx2Pt, // left
|
|
2343
|
-
parsedPadding[1] / ratioPx2Pt, // right
|
|
2344
|
-
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2345
|
-
parsedPadding[0] / ratioPx2Pt // top
|
|
2346
|
-
]
|
|
2347
2141
|
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
pptxSlide.addImage(imgOpts)
|
|
2146
|
+
}
|
|
2147
|
+
else if (el.type === 'shape') {
|
|
2148
|
+
const shapeType = getPredefinedShapeType(el.content, el.pathFormula, (el as any).pptxShapeType)
|
|
2149
|
+
|
|
2150
|
+
// Set defaults based on shape types to match CanvasArea.vue styles
|
|
2151
|
+
let defaultFill = '#3b82f6'
|
|
2152
|
+
let defaultBorderColor = '#2563eb'
|
|
2153
|
+
let defaultBorderWidth = '2px'
|
|
2154
|
+
|
|
2155
|
+
if (el.content === 'triangle') {
|
|
2156
|
+
defaultFill = '#f59e0b'
|
|
2157
|
+
defaultBorderColor = '#d97706'
|
|
2158
|
+
defaultBorderWidth = '3px'
|
|
2159
|
+
} else if (el.content === 'circle') {
|
|
2160
|
+
defaultFill = '#3b82f6'
|
|
2161
|
+
defaultBorderColor = '#2563eb'
|
|
2162
|
+
defaultBorderWidth = '2px'
|
|
2163
|
+
} else if (typeof el.content === 'string' && (el.content.startsWith('M') || el.content.startsWith('m'))) {
|
|
2164
|
+
defaultFill = '#3b82f6'
|
|
2165
|
+
defaultBorderColor = '#2563eb'
|
|
2166
|
+
defaultBorderWidth = '2px'
|
|
2167
|
+
} else { // rect or default
|
|
2168
|
+
defaultFill = '#10b981'
|
|
2169
|
+
defaultBorderColor = '#059669'
|
|
2170
|
+
defaultBorderWidth = '2px'
|
|
2171
|
+
}
|
|
2348
2172
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
if (
|
|
2359
|
-
const val =
|
|
2360
|
-
|
|
2361
|
-
:
|
|
2362
|
-
|
|
2363
|
-
|
|
2173
|
+
const style: any = el.style || {}
|
|
2174
|
+
let shapeFillOpt: any = undefined
|
|
2175
|
+
|
|
2176
|
+
// Helper function to extract element gradient configuration for PPTX export
|
|
2177
|
+
const getElementGradientConfig = (item: any): any => {
|
|
2178
|
+
if (!item) return undefined
|
|
2179
|
+
let gradient = item.gradient || item.style?.gradient
|
|
2180
|
+
if (!gradient) {
|
|
2181
|
+
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2182
|
+
if (rawFill && typeof rawFill === 'object' && rawFill.type === 'gradient') {
|
|
2183
|
+
const val = rawFill.value || {}
|
|
2184
|
+
gradient = {
|
|
2185
|
+
path: val.path || 'line',
|
|
2186
|
+
type: val.path === 'line' || !val.path ? 'linear' : 'radial',
|
|
2187
|
+
rotate: val.rot !== undefined ? val.rot : (val.rotate !== undefined ? val.rotate : 90),
|
|
2188
|
+
colors: (val.colors || []).map((c: any) => ({
|
|
2189
|
+
color: c.color || c.value || '#3b82f6',
|
|
2190
|
+
pos: c.pos !== undefined ? (typeof c.pos === 'string' ? parseInt(c.pos) : c.pos) : 0
|
|
2191
|
+
}))
|
|
2364
2192
|
}
|
|
2365
2193
|
}
|
|
2194
|
+
}
|
|
2195
|
+
return gradient
|
|
2196
|
+
}
|
|
2366
2197
|
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2198
|
+
const getElementPatternUrl = (item: any): string | undefined => {
|
|
2199
|
+
if (!item) return undefined
|
|
2200
|
+
if (item.pattern || item.style?.pattern) return item.pattern || item.style?.pattern
|
|
2201
|
+
const rawFill = (item.style && item.style.fill !== undefined) ? item.style.fill : item.fill
|
|
2202
|
+
if (rawFill && typeof rawFill === 'object' && (rawFill.type === 'image' || rawFill.type === 'pattern')) {
|
|
2203
|
+
return rawFill.value?.src || rawFill.value?.url || rawFill.value
|
|
2204
|
+
}
|
|
2205
|
+
return undefined
|
|
2206
|
+
}
|
|
2372
2207
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2208
|
+
const patternUrl = getElementPatternUrl(el)
|
|
2209
|
+
const g = getElementGradientConfig(el)
|
|
2210
|
+
|
|
2211
|
+
// SVG 图片填充 PowerPoint 无法可靠渲染 a:blipFill,先按形状显示尺寸栅格化为 PNG
|
|
2212
|
+
// (画布上图案填充本身是 preserveAspectRatio="none" 拉伸铺满,与 a:stretch 语义一致)
|
|
2213
|
+
const rasterizeSvgFill = (svgUrl: string, widthPx: number, heightPx: number): Promise<string | undefined> => {
|
|
2214
|
+
return new Promise((resolve) => {
|
|
2215
|
+
const img = new Image()
|
|
2216
|
+
img.onload = () => {
|
|
2217
|
+
try {
|
|
2218
|
+
const canvas = document.createElement('canvas')
|
|
2219
|
+
canvas.width = Math.max(1, Math.min(4096, Math.round(widthPx)))
|
|
2220
|
+
canvas.height = Math.max(1, Math.min(4096, Math.round(heightPx)))
|
|
2221
|
+
const ctx = canvas.getContext('2d')
|
|
2222
|
+
if (!ctx) return resolve(undefined)
|
|
2223
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
|
|
2224
|
+
resolve(canvas.toDataURL('image/png'))
|
|
2225
|
+
} catch {
|
|
2226
|
+
resolve(undefined)
|
|
2227
|
+
}
|
|
2377
2228
|
}
|
|
2229
|
+
img.onerror = () => resolve(undefined)
|
|
2230
|
+
img.src = svgUrl
|
|
2231
|
+
})
|
|
2232
|
+
}
|
|
2378
2233
|
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
strike: isShapeTextStrike,
|
|
2387
|
-
align: shapeAlign,
|
|
2388
|
-
shadow: parsedShapeTextShadow,
|
|
2389
|
-
reflection: parsedShapeTextReflection
|
|
2390
|
-
})
|
|
2391
|
-
const shapeTextOpts: any = {
|
|
2392
|
-
x, y, w, h,
|
|
2393
|
-
shape: shapeType,
|
|
2394
|
-
valign: shapeValign as any,
|
|
2395
|
-
align: shapeAlign as any,
|
|
2396
|
-
margin: shapeMargin,
|
|
2397
|
-
rotate, flipH, flipV,
|
|
2398
|
-
objectName: el.id,
|
|
2399
|
-
fill: nativeFillOpt || shapeFillOpt
|
|
2400
|
-
}
|
|
2401
|
-
if (shapeOpts.points) {
|
|
2402
|
-
shapeTextOpts.points = shapeOpts.points
|
|
2403
|
-
}
|
|
2404
|
-
if (shapeOpts.line) {
|
|
2405
|
-
shapeTextOpts.line = shapeOpts.line
|
|
2406
|
-
}
|
|
2407
|
-
if (shapeOpts.shadow) {
|
|
2408
|
-
shapeTextOpts.shadow = shapeOpts.shadow
|
|
2409
|
-
}
|
|
2410
|
-
if (shapeOpts.reflection) {
|
|
2411
|
-
shapeTextOpts.reflection = shapeOpts.reflection
|
|
2412
|
-
}
|
|
2413
|
-
if (finalLineHeight !== undefined) {
|
|
2414
|
-
shapeTextOpts.lineSpacingMultiple = finalLineHeight
|
|
2234
|
+
const buildNativeShapeFill = async (): Promise<any> => {
|
|
2235
|
+
// 图片填充 → 原生 a:blipFill
|
|
2236
|
+
if (patternUrl) {
|
|
2237
|
+
const pUrl = await resolveMediaSrc(patternUrl, 'image')
|
|
2238
|
+
if (pUrl && /^data:image\/svg\+xml/i.test(pUrl)) {
|
|
2239
|
+
const pngUrl = await rasterizeSvgFill(pUrl, pixelWidth, pixelHeight)
|
|
2240
|
+
return pngUrl ? { type: 'image', data: pngUrl } : undefined
|
|
2415
2241
|
}
|
|
2416
|
-
if (
|
|
2417
|
-
|
|
2242
|
+
if (pUrl && pUrl.startsWith('data:')) {
|
|
2243
|
+
return { type: 'image', data: pUrl }
|
|
2418
2244
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
if (elAny.autoFit) {
|
|
2422
|
-
if (elAny.autoFit.type === 'shape') {
|
|
2423
|
-
shapeTextOpts.fit = 'resize'
|
|
2424
|
-
} else if (elAny.autoFit.type === 'text') {
|
|
2425
|
-
shapeTextOpts.fit = 'shrink'
|
|
2426
|
-
}
|
|
2245
|
+
if (pUrl && /^https?:\/\//.test(pUrl)) {
|
|
2246
|
+
return { type: 'image', path: pUrl }
|
|
2427
2247
|
}
|
|
2428
|
-
|
|
2429
|
-
pptxSlide.addText(textProps, shapeTextOpts)
|
|
2430
|
-
} else {
|
|
2431
|
-
pptxSlide.addShape(shapeType as any, shapeOpts)
|
|
2248
|
+
return undefined // 图片解析失败,回落纯色
|
|
2432
2249
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
if (rawType === 'pieChart' || rawType === 'pie3DChart') rawType = 'pie'
|
|
2444
|
-
if (rawType === 'barChart' || rawType === 'bar3DChart') rawType = 'bar'
|
|
2445
|
-
if (rawType === 'lineChart' || rawType === 'line3DChart') rawType = 'line'
|
|
2446
|
-
if (rawType === 'areaChart' || rawType === 'area3DChart') rawType = 'area'
|
|
2447
|
-
|
|
2448
|
-
let chartType = pptx.ChartType.bar
|
|
2449
|
-
if (rawType === 'line') {
|
|
2450
|
-
chartType = pptx.ChartType.line
|
|
2451
|
-
} else if (rawType === 'area') {
|
|
2452
|
-
chartType = pptx.ChartType.area
|
|
2453
|
-
} else if (rawType === 'scatter') {
|
|
2454
|
-
chartType = pptx.ChartType.scatter
|
|
2455
|
-
} else if (rawType === 'pie') {
|
|
2456
|
-
chartType = pptx.ChartType.pie
|
|
2457
|
-
} else if (rawType === 'donut') {
|
|
2458
|
-
chartType = pptx.ChartType.doughnut
|
|
2459
|
-
} else if (rawType === 'radar') {
|
|
2460
|
-
chartType = pptx.ChartType.radar
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
// 1. Resolve colors
|
|
2464
|
-
let colorsList: string[] = []
|
|
2465
|
-
if (Array.isArray(el.style?.chartColors) && el.style.chartColors.length > 0) {
|
|
2466
|
-
colorsList = el.style.chartColors
|
|
2467
|
-
} else if (Array.isArray((el as any).themeColors) && (el as any).themeColors.length > 0) {
|
|
2468
|
-
colorsList = (el as any).themeColors
|
|
2469
|
-
} else if (Array.isArray((el as any).colors) && (el as any).colors.length > 0) {
|
|
2470
|
-
colorsList = (el as any).colors
|
|
2471
|
-
} else if (Array.isArray(el.data?.datasets?.[0]?.backgroundColor) && el.data.datasets[0].backgroundColor.length > 0) {
|
|
2472
|
-
colorsList = el.data.datasets[0].backgroundColor
|
|
2473
|
-
} else if (Array.isArray(el.data?.datasets?.[0]?.colors) && el.data.datasets[0].colors.length > 0) {
|
|
2474
|
-
colorsList = el.data.datasets[0].colors
|
|
2475
|
-
} else {
|
|
2476
|
-
const dsColors = (el.data?.datasets || []).map((ds: any) => ds.color || ds.backgroundColor).filter((c: any) => c && typeof c === 'string')
|
|
2477
|
-
if (dsColors.length > 0) {
|
|
2478
|
-
colorsList = dsColors
|
|
2479
|
-
} else {
|
|
2480
|
-
colorsList = (globalThemeColor && globalThemeColor.value && globalThemeColor.value.length > 0)
|
|
2481
|
-
? globalThemeColor.value
|
|
2482
|
-
: ['#4f46e5', '#06b6d4', '#f59e0b', '#10b981', '#ec4899', '#8b5cf6', '#ef4444', '#14b8a6']
|
|
2250
|
+
// 渐变填充 → 原生 a:gradFill
|
|
2251
|
+
if (g && Array.isArray(g.colors) && g.colors.length >= 2) {
|
|
2252
|
+
return {
|
|
2253
|
+
type: 'gradient',
|
|
2254
|
+
stops: g.colors.map((c: any) => ({
|
|
2255
|
+
color: String(c.color || '#3b82f6').replace('#', ''),
|
|
2256
|
+
pos: typeof c.pos === 'number' && !isNaN(c.pos) ? c.pos : 0
|
|
2257
|
+
})),
|
|
2258
|
+
angle: g.rotate !== undefined ? g.rotate : (g.rot !== undefined ? g.rot : 90),
|
|
2259
|
+
radial: g.type === 'radial' || g.path === 'circle'
|
|
2483
2260
|
}
|
|
2484
2261
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
// 2. Legend visibility and position
|
|
2490
|
-
const isPieOrDonut = chartType === pptx.ChartType.doughnut || chartType === pptx.ChartType.pie
|
|
2491
|
-
let showLegend = false
|
|
2492
|
-
if (el.showLegend !== undefined) {
|
|
2493
|
-
showLegend = Boolean(el.showLegend)
|
|
2494
|
-
} else if ((el as any).options?.showLegend !== undefined) {
|
|
2495
|
-
showLegend = Boolean((el as any).options.showLegend)
|
|
2496
|
-
} else {
|
|
2497
|
-
showLegend = !isPieOrDonut
|
|
2498
|
-
}
|
|
2262
|
+
return undefined
|
|
2263
|
+
}
|
|
2264
|
+
const nativeFillOpt = await buildNativeShapeFill()
|
|
2499
2265
|
|
|
2500
|
-
|
|
2501
|
-
|
|
2266
|
+
const rawFill = style.fill !== undefined ? style.fill : (el.fill !== undefined ? el.fill : defaultFill)
|
|
2267
|
+
if (rawFill === 'transparent' || rawFill === 'none') {
|
|
2268
|
+
shapeFillOpt = { type: 'none' }
|
|
2269
|
+
} else {
|
|
2270
|
+
const fillParsed = parseColor(rawFill)
|
|
2271
|
+
shapeFillOpt = { color: fillParsed.color, transparency: fillParsed.transparency }
|
|
2272
|
+
}
|
|
2502
2273
|
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2274
|
+
const shapeOpts: pptxgen.ShapeProps = {
|
|
2275
|
+
x, y, w, h,
|
|
2276
|
+
fill: nativeFillOpt || shapeFillOpt,
|
|
2277
|
+
rotate, flipH, flipV,
|
|
2278
|
+
objectName: el.id
|
|
2279
|
+
}
|
|
2509
2280
|
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
chartOpts.legendColor = textColorHex
|
|
2519
|
-
chartOpts.legendFontSize = 10
|
|
2520
|
-
chartOpts.legendFontFace = chartFont
|
|
2281
|
+
const borderWidth = style.borderWidth ? (parseFloat(style.borderWidth) || 0) : 0
|
|
2282
|
+
const borderColorStr = style.borderColor
|
|
2283
|
+
if (borderWidth > 0 && borderColorStr && borderColorStr !== 'transparent' && borderColorStr !== 'none') {
|
|
2284
|
+
const borderParsed = parseColor(borderColorStr)
|
|
2285
|
+
shapeOpts.line = {
|
|
2286
|
+
color: borderParsed.color,
|
|
2287
|
+
width: borderWidth / ratioPx2Pt,
|
|
2288
|
+
dashType: style.borderStyle === 'dashed' ? 'dash' : 'solid'
|
|
2521
2289
|
}
|
|
2290
|
+
}
|
|
2522
2291
|
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2292
|
+
const shapeShadow = el.shadow || style.shadow
|
|
2293
|
+
let parsedShadow: any = convertElShadowToPptx(shapeShadow, ratioPx2Pt)
|
|
2294
|
+
if (!parsedShadow && style.boxShadow) {
|
|
2295
|
+
parsedShadow = parseCssShadow(style.boxShadow, ratioPx2Pt)
|
|
2296
|
+
}
|
|
2297
|
+
if (parsedShadow) {
|
|
2298
|
+
shapeOpts.shadow = parsedShadow
|
|
2299
|
+
}
|
|
2529
2300
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2301
|
+
const shapeRefl = el.reflection || style.reflection
|
|
2302
|
+
if (shapeRefl) {
|
|
2303
|
+
const parsedReflection = convertElReflectionToPptx(shapeRefl, ratioPx2Pt)
|
|
2304
|
+
if (parsedReflection) {
|
|
2305
|
+
shapeOpts.reflection = parsedReflection
|
|
2535
2306
|
}
|
|
2307
|
+
}
|
|
2536
2308
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2309
|
+
if (shapeType === 'custGeom') {
|
|
2310
|
+
const rawPoints = toPoints(el.content || '')
|
|
2311
|
+
const viewBoxWidth = (Array.isArray(el.viewBox) && el.viewBox.length >= 2) ? el.viewBox[0] : pixelWidth
|
|
2312
|
+
const viewBoxHeight = (Array.isArray(el.viewBox) && el.viewBox.length >= 2) ? el.viewBox[1] : pixelHeight
|
|
2313
|
+
const scale = {
|
|
2314
|
+
x: pixelWidth / viewBoxWidth,
|
|
2315
|
+
y: viewBoxHeight > 0 ? pixelHeight / viewBoxHeight : 1
|
|
2543
2316
|
}
|
|
2317
|
+
shapeOpts.points = formatPoints(rawPoints, scale, pixelToInch) as any
|
|
2318
|
+
}
|
|
2544
2319
|
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
} else {
|
|
2568
|
-
chartOpts.showPercent = false
|
|
2569
|
-
chartOpts.showValue = false
|
|
2570
|
-
}
|
|
2571
|
-
} else {
|
|
2572
|
-
if (showDataLabels || (el as any).showValue) {
|
|
2573
|
-
chartOpts.showValue = true
|
|
2574
|
-
chartOpts.dataLabelColor = textColorHex
|
|
2575
|
-
chartOpts.dataLabelFontSize = 10
|
|
2576
|
-
chartOpts.dataLabelFontFace = chartFont
|
|
2320
|
+
const shapeText = typeof el.text === 'string' ? el.text : ((el as any).text?.content || (typeof el.content === 'string' && !el.content.startsWith('M') && !el.content.startsWith('m') && el.content !== 'circle' && el.content !== 'triangle' && el.content.replace(/<[^>]+>/g, '').trim().length > 0 ? el.content : ''))
|
|
2321
|
+
|
|
2322
|
+
if (shapeText) {
|
|
2323
|
+
const shapeTextDefaultColor = style.textColor || style.color || (style.fill === '#ffffff' || style.fill === '#FFFFFF' ? '#475569' : '#ffffff')
|
|
2324
|
+
const shapeTextDefaultFont = style.fontFamily || (globalFontFamily && globalFontFamily.value) || 'Arial'
|
|
2325
|
+
const resolvedShapeFont = (shapeTextDefaultFont !== 'inherit' && shapeTextDefaultFont !== '默认字体') ? shapeTextDefaultFont : 'Microsoft YaHei'
|
|
2326
|
+
const shapeTextDefaultSize = Math.max(1, Math.round((parseFloat(style.fontSize || '') || 16) * fontSizeFactor))
|
|
2327
|
+
|
|
2328
|
+
const isShapeTextBold = style.fontWeight === 'bold' || style.fontWeight === '700' || (parseInt(style.fontWeight as string) || 0) > 500
|
|
2329
|
+
const isShapeTextItalic = style.fontStyle === 'italic'
|
|
2330
|
+
const isShapeTextUnderline = !!(style.textDecoration && style.textDecoration.includes('underline'))
|
|
2331
|
+
const isShapeTextStrike = !!(style.textDecoration && style.textDecoration.includes('line-through'))
|
|
2332
|
+
|
|
2333
|
+
let shapeMargin: [number, number, number, number] = [7.2, 7.2, 3.6, 3.6]
|
|
2334
|
+
if (style.padding) {
|
|
2335
|
+
const paddingParts = style.padding.trim().split(/\s+/)
|
|
2336
|
+
let parsedPadding = [0, 0, 0, 0]
|
|
2337
|
+
if (paddingParts.length === 4) {
|
|
2338
|
+
parsedPadding = paddingParts.map((p: string) => parseFloat(p) || 0)
|
|
2339
|
+
} else if (paddingParts.length === 1) {
|
|
2340
|
+
const val = parseFloat(paddingParts[0]) || 0
|
|
2341
|
+
parsedPadding = [val, val, val, val]
|
|
2577
2342
|
}
|
|
2343
|
+
shapeMargin = [
|
|
2344
|
+
parsedPadding[3] / ratioPx2Pt, // left
|
|
2345
|
+
parsedPadding[1] / ratioPx2Pt, // right
|
|
2346
|
+
parsedPadding[2] / ratioPx2Pt, // bottom
|
|
2347
|
+
parsedPadding[0] / ratioPx2Pt // top
|
|
2348
|
+
]
|
|
2578
2349
|
}
|
|
2579
2350
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2351
|
+
let shapeValign = 'middle'
|
|
2352
|
+
const currentShapeValign = el.style?.verticalAlign || (el as any).vAlign
|
|
2353
|
+
if (currentShapeValign === 'top' || currentShapeValign === 'up') shapeValign = 'top'
|
|
2354
|
+
else if (currentShapeValign === 'bottom' || currentShapeValign === 'down') shapeValign = 'bottom'
|
|
2355
|
+
else if (currentShapeValign === 'middle' || currentShapeValign === 'mid') shapeValign = 'middle'
|
|
2585
2356
|
|
|
2586
|
-
|
|
2587
|
-
chartOpts.catGridLine = { style: 'none' }
|
|
2357
|
+
let shapeAlign = style.textAlign || 'center'
|
|
2588
2358
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2359
|
+
let finalLineHeight: number | undefined = undefined
|
|
2360
|
+
if (style.lineHeight !== undefined) {
|
|
2361
|
+
const val = typeof style.lineHeight === 'number'
|
|
2362
|
+
? style.lineHeight
|
|
2363
|
+
: parseFloat(style.lineHeight as string || '')
|
|
2364
|
+
if (!isNaN(val)) {
|
|
2365
|
+
finalLineHeight = val <= 1.2 ? 1.0 : val
|
|
2366
|
+
}
|
|
2593
2367
|
}
|
|
2594
2368
|
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2369
|
+
let shapeTextShadowObj = (el as any).textShadow || (el.style && (el.style as any).textShadow)
|
|
2370
|
+
let parsedShapeTextShadow: any = undefined
|
|
2371
|
+
if (shapeTextShadowObj) {
|
|
2372
|
+
parsedShapeTextShadow = convertElShadowToPptx(shapeTextShadowObj, ratioPx2Pt)
|
|
2599
2373
|
}
|
|
2600
2374
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
const lineOpts: any = {
|
|
2375
|
+
let shapeTextReflObj = (el as any).textReflection || (el.style as any)?.textReflection
|
|
2376
|
+
let parsedShapeTextReflection: any = undefined
|
|
2377
|
+
if (shapeTextReflObj && shapeTextReflObj.enabled !== false) {
|
|
2378
|
+
parsedShapeTextReflection = convertElReflectionToPptx(shapeTextReflObj, ratioPx2Pt)
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const textProps = formatHTMLToPPTX(shapeText, {
|
|
2382
|
+
fontSize: shapeTextDefaultSize,
|
|
2383
|
+
color: shapeTextDefaultColor,
|
|
2384
|
+
fontFamily: resolvedShapeFont,
|
|
2385
|
+
bold: isShapeTextBold,
|
|
2386
|
+
italic: isShapeTextItalic,
|
|
2387
|
+
underline: isShapeTextUnderline,
|
|
2388
|
+
strike: isShapeTextStrike,
|
|
2389
|
+
align: shapeAlign,
|
|
2390
|
+
shadow: parsedShapeTextShadow,
|
|
2391
|
+
reflection: parsedShapeTextReflection
|
|
2392
|
+
})
|
|
2393
|
+
const shapeTextOpts: any = {
|
|
2622
2394
|
x, y, w, h,
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
beginArrowType: beginArrow as any,
|
|
2628
|
-
endArrowType: endArrow as any
|
|
2629
|
-
},
|
|
2630
|
-
points,
|
|
2395
|
+
shape: shapeType,
|
|
2396
|
+
valign: shapeValign as any,
|
|
2397
|
+
align: shapeAlign as any,
|
|
2398
|
+
margin: shapeMargin,
|
|
2631
2399
|
rotate, flipH, flipV,
|
|
2632
|
-
objectName: el.id
|
|
2400
|
+
objectName: el.id,
|
|
2401
|
+
fill: nativeFillOpt || shapeFillOpt
|
|
2633
2402
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
if (!parsedLineShadow && el.style?.boxShadow) {
|
|
2637
|
-
parsedLineShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2403
|
+
if (shapeOpts.points) {
|
|
2404
|
+
shapeTextOpts.points = shapeOpts.points
|
|
2638
2405
|
}
|
|
2639
|
-
if (
|
|
2640
|
-
|
|
2641
|
-
if (lineRefl) {
|
|
2642
|
-
const parsedReflection = convertElReflectionToPptx(lineRefl, ratioPx2Pt)
|
|
2643
|
-
if (parsedReflection) lineOpts.reflection = parsedReflection
|
|
2406
|
+
if (shapeOpts.line) {
|
|
2407
|
+
shapeTextOpts.line = shapeOpts.line
|
|
2644
2408
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2409
|
+
if (shapeOpts.shadow) {
|
|
2410
|
+
shapeTextOpts.shadow = shapeOpts.shadow
|
|
2411
|
+
}
|
|
2412
|
+
if (shapeOpts.reflection) {
|
|
2413
|
+
shapeTextOpts.reflection = shapeOpts.reflection
|
|
2414
|
+
}
|
|
2415
|
+
if (finalLineHeight !== undefined) {
|
|
2416
|
+
shapeTextOpts.lineSpacingMultiple = finalLineHeight
|
|
2417
|
+
}
|
|
2418
|
+
if (style.whiteSpace === 'nowrap') {
|
|
2419
|
+
shapeTextOpts.wrap = false
|
|
2656
2420
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
if (
|
|
2663
|
-
|
|
2664
|
-
else if (mediaPath.includes('audio/m4a') || mediaPath.includes('audio/x-m4a') || mediaPath.includes('audio/mp4')) extn = 'm4a'
|
|
2665
|
-
else if (mediaPath.includes('audio/ogg')) extn = 'ogg'
|
|
2666
|
-
else extn = 'mp3'
|
|
2667
|
-
} else if (mediaPath.startsWith('data:video/')) {
|
|
2668
|
-
if (mediaPath.includes('video/webm')) extn = 'webm'
|
|
2669
|
-
else if (mediaPath.includes('video/ogg')) extn = 'ogg'
|
|
2670
|
-
else extn = 'mp4'
|
|
2421
|
+
|
|
2422
|
+
const elAny = el as any
|
|
2423
|
+
if (elAny.autoFit) {
|
|
2424
|
+
if (elAny.autoFit.type === 'shape') {
|
|
2425
|
+
shapeTextOpts.fit = 'resize'
|
|
2426
|
+
} else if (elAny.autoFit.type === 'text') {
|
|
2427
|
+
shapeTextOpts.fit = 'shrink'
|
|
2671
2428
|
}
|
|
2429
|
+
}
|
|
2672
2430
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2431
|
+
pptxSlide.addText(textProps, shapeTextOpts)
|
|
2432
|
+
} else {
|
|
2433
|
+
pptxSlide.addShape(shapeType as any, shapeOpts)
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
else if (el.type === 'chart') {
|
|
2437
|
+
const chartData = el.data?.datasets.map((ds: any, idx: number) => ({
|
|
2438
|
+
name: ds.label || `Series ${idx + 1}`,
|
|
2439
|
+
labels: el.data?.labels || [],
|
|
2440
|
+
values: ds.data || []
|
|
2441
|
+
})) || []
|
|
2442
|
+
|
|
2443
|
+
let rawType = (el.chartType as string) || 'bar'
|
|
2444
|
+
if (rawType === 'doughnutChart' || rawType === 'ring') rawType = 'donut'
|
|
2445
|
+
if (rawType === 'pieChart' || rawType === 'pie3DChart') rawType = 'pie'
|
|
2446
|
+
if (rawType === 'barChart' || rawType === 'bar3DChart') rawType = 'bar'
|
|
2447
|
+
if (rawType === 'lineChart' || rawType === 'line3DChart') rawType = 'line'
|
|
2448
|
+
if (rawType === 'areaChart' || rawType === 'area3DChart') rawType = 'area'
|
|
2449
|
+
|
|
2450
|
+
let chartType = pptx.ChartType.bar
|
|
2451
|
+
if (rawType === 'line') {
|
|
2452
|
+
chartType = pptx.ChartType.line
|
|
2453
|
+
} else if (rawType === 'area') {
|
|
2454
|
+
chartType = pptx.ChartType.area
|
|
2455
|
+
} else if (rawType === 'scatter') {
|
|
2456
|
+
chartType = pptx.ChartType.scatter
|
|
2457
|
+
} else if (rawType === 'pie') {
|
|
2458
|
+
chartType = pptx.ChartType.pie
|
|
2459
|
+
} else if (rawType === 'donut') {
|
|
2460
|
+
chartType = pptx.ChartType.doughnut
|
|
2461
|
+
} else if (rawType === 'radar') {
|
|
2462
|
+
chartType = pptx.ChartType.radar
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// 1. Resolve colors
|
|
2466
|
+
let colorsList: string[] = []
|
|
2467
|
+
if (Array.isArray(el.style?.chartColors) && el.style.chartColors.length > 0) {
|
|
2468
|
+
colorsList = el.style.chartColors
|
|
2469
|
+
} else if (Array.isArray((el as any).themeColors) && (el as any).themeColors.length > 0) {
|
|
2470
|
+
colorsList = (el as any).themeColors
|
|
2471
|
+
} else if (Array.isArray((el as any).colors) && (el as any).colors.length > 0) {
|
|
2472
|
+
colorsList = (el as any).colors
|
|
2473
|
+
} else if (Array.isArray(el.data?.datasets?.[0]?.backgroundColor) && el.data.datasets[0].backgroundColor.length > 0) {
|
|
2474
|
+
colorsList = el.data.datasets[0].backgroundColor
|
|
2475
|
+
} else if (Array.isArray(el.data?.datasets?.[0]?.colors) && el.data.datasets[0].colors.length > 0) {
|
|
2476
|
+
colorsList = el.data.datasets[0].colors
|
|
2477
|
+
} else {
|
|
2478
|
+
const dsColors = (el.data?.datasets || []).map((ds: any) => ds.color || ds.backgroundColor).filter((c: any) => c && typeof c === 'string')
|
|
2479
|
+
if (dsColors.length > 0) {
|
|
2480
|
+
colorsList = dsColors
|
|
2680
2481
|
} else {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2482
|
+
colorsList = (globalThemeColor && globalThemeColor.value && globalThemeColor.value.length > 0)
|
|
2483
|
+
? globalThemeColor.value
|
|
2484
|
+
: ['#4f46e5', '#06b6d4', '#f59e0b', '#10b981', '#ec4899', '#8b5cf6', '#ef4444', '#14b8a6']
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const cleanHexColors = colorsList
|
|
2488
|
+
.filter((c: any) => c && typeof c === 'string')
|
|
2489
|
+
.map((c: string) => parseColor(c).color)
|
|
2490
|
+
|
|
2491
|
+
// 2. Legend visibility and position
|
|
2492
|
+
const isPieOrDonut = chartType === pptx.ChartType.doughnut || chartType === pptx.ChartType.pie
|
|
2493
|
+
let showLegend = false
|
|
2494
|
+
if (el.showLegend !== undefined) {
|
|
2495
|
+
showLegend = Boolean(el.showLegend)
|
|
2496
|
+
} else if ((el as any).options?.showLegend !== undefined) {
|
|
2497
|
+
showLegend = Boolean((el as any).options.showLegend)
|
|
2498
|
+
} else {
|
|
2499
|
+
showLegend = !isPieOrDonut
|
|
2500
|
+
}
|
|
2696
2501
|
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2502
|
+
const chartFont = el.style?.fontFamily || el.fontFamily || (globalFontFamily && globalFontFamily.value) || 'Microsoft YaHei'
|
|
2503
|
+
const textColorHex = parseColor(el.style?.color || el.style?.textColor || el.textColor || '#64748b').color
|
|
2504
|
+
|
|
2505
|
+
const chartOpts: pptxgen.IChartOpts = {
|
|
2506
|
+
x, y, w, h,
|
|
2507
|
+
showLegend,
|
|
2508
|
+
chartColors: cleanHexColors.length > 0 ? cleanHexColors : undefined,
|
|
2509
|
+
objectName: el.id
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
if (showLegend) {
|
|
2513
|
+
let legendPos: 'b' | 'l' | 'r' | 't' = 'b'
|
|
2514
|
+
const rawPos = (el.legendPosition || (el as any).options?.legendPosition || 'bottom').toLowerCase()
|
|
2515
|
+
if (rawPos.startsWith('top')) legendPos = 't'
|
|
2516
|
+
else if (rawPos.startsWith('left')) legendPos = 'l'
|
|
2517
|
+
else if (rawPos.startsWith('right')) legendPos = 'r'
|
|
2518
|
+
else if (rawPos.startsWith('bottom')) legendPos = 'b'
|
|
2519
|
+
chartOpts.legendPos = legendPos
|
|
2520
|
+
chartOpts.legendColor = textColorHex
|
|
2521
|
+
chartOpts.legendFontSize = 10
|
|
2522
|
+
chartOpts.legendFontFace = chartFont
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// 3. Bar direction
|
|
2526
|
+
if (rawType === 'horizontal-bar') {
|
|
2527
|
+
chartOpts.barDir = 'bar'
|
|
2528
|
+
} else if (rawType === 'bar') {
|
|
2529
|
+
chartOpts.barDir = 'col'
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// 4. Donut specific options (hole size)
|
|
2533
|
+
if (chartType === pptx.ChartType.doughnut) {
|
|
2534
|
+
const rawHole = parseFloat((el as any).holeSize || (el as any).options?.holeSize)
|
|
2535
|
+
const holeSize = (!isNaN(rawHole) && rawHole > 0 && rawHole < 100) ? Math.round(rawHole) : 72
|
|
2536
|
+
chartOpts.holeSize = holeSize
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// 5. Slices border
|
|
2540
|
+
if (isPieOrDonut) {
|
|
2541
|
+
chartOpts.dataBorder = {
|
|
2542
|
+
pt: 1.5,
|
|
2543
|
+
color: 'FFFFFF'
|
|
2707
2544
|
}
|
|
2708
2545
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2546
|
+
|
|
2547
|
+
// 6. Data labels & Percent
|
|
2548
|
+
const hasPercentOpt = (el as any).showPercent !== undefined
|
|
2549
|
+
? Boolean((el as any).showPercent)
|
|
2550
|
+
: (el as any).options?.showPercent !== undefined
|
|
2551
|
+
? Boolean((el as any).options.showPercent)
|
|
2552
|
+
: isPieOrDonut
|
|
2553
|
+
|
|
2554
|
+
const showDataLabels = el.showDataLabels !== undefined
|
|
2555
|
+
? Boolean(el.showDataLabels)
|
|
2556
|
+
: (el as any).options?.showDataLabels !== undefined
|
|
2557
|
+
? Boolean((el as any).options.showDataLabels)
|
|
2558
|
+
: isPieOrDonut
|
|
2559
|
+
|
|
2560
|
+
if (isPieOrDonut) {
|
|
2561
|
+
if (hasPercentOpt || showDataLabels) {
|
|
2562
|
+
chartOpts.showPercent = true
|
|
2563
|
+
chartOpts.showValue = false
|
|
2564
|
+
chartOpts.dataLabelColor = 'FFFFFF'
|
|
2565
|
+
chartOpts.dataLabelFontSize = 11
|
|
2566
|
+
chartOpts.dataLabelFontBold = true
|
|
2567
|
+
chartOpts.dataLabelFormatCode = '0%'
|
|
2568
|
+
chartOpts.dataLabelFontFace = chartFont
|
|
2569
|
+
} else {
|
|
2570
|
+
chartOpts.showPercent = false
|
|
2571
|
+
chartOpts.showValue = false
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
if (showDataLabels || (el as any).showValue) {
|
|
2575
|
+
chartOpts.showValue = true
|
|
2576
|
+
chartOpts.dataLabelColor = textColorHex
|
|
2577
|
+
chartOpts.dataLabelFontSize = 10
|
|
2578
|
+
chartOpts.dataLabelFontFace = chartFont
|
|
2719
2579
|
}
|
|
2720
2580
|
}
|
|
2581
|
+
|
|
2582
|
+
// 7. Cartesian axes & grid lines
|
|
2583
|
+
if (!isPieOrDonut && chartType !== pptx.ChartType.radar) {
|
|
2584
|
+
const gridHex = parseColor(el.style?.gridColor || el.gridColor || '#e2e8f0').color
|
|
2585
|
+
const showGrid = el.showGridlines !== false
|
|
2586
|
+
const showAxes = el.showAxes !== false
|
|
2587
|
+
|
|
2588
|
+
chartOpts.valGridLine = showGrid ? { color: gridHex, size: 0.75, style: 'dash' } : { style: 'none' }
|
|
2589
|
+
chartOpts.catGridLine = { style: 'none' }
|
|
2590
|
+
|
|
2591
|
+
chartOpts.catAxisLabelColor = textColorHex
|
|
2592
|
+
chartOpts.valAxisLabelColor = textColorHex
|
|
2593
|
+
chartOpts.catAxisHidden = !showAxes
|
|
2594
|
+
chartOpts.valAxisHidden = !showAxes
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// 8. Line chart specific
|
|
2598
|
+
if (chartType === pptx.ChartType.line) {
|
|
2599
|
+
chartOpts.lineSize = 2.5
|
|
2600
|
+
chartOpts.lineSmooth = true
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
pptxSlide.addChart(chartType, chartData, chartOpts)
|
|
2604
|
+
}
|
|
2605
|
+
else if (el.type === 'line') {
|
|
2606
|
+
const mapArrowToPPTX = (uiType: string | undefined) => {
|
|
2607
|
+
if (!uiType) return 'none'
|
|
2608
|
+
const t = uiType.toLowerCase()
|
|
2609
|
+
if (t === 'arrow') return 'triangle'
|
|
2610
|
+
if (t === 'open-arrow') return 'arrow'
|
|
2611
|
+
if (t === 'stealth-arrow') return 'stealth'
|
|
2612
|
+
if (t === 'dot') return 'oval'
|
|
2613
|
+
if (t === 'diamond') return 'diamond'
|
|
2614
|
+
return 'none'
|
|
2615
|
+
}
|
|
2616
|
+
const beginArrow = mapArrowToPPTX(el.points?.[0])
|
|
2617
|
+
const endArrow = mapArrowToPPTX(el.points?.[1])
|
|
2618
|
+
|
|
2619
|
+
const path = getLineElementPath(el)
|
|
2620
|
+
const rawPoints = toPoints(path)
|
|
2621
|
+
const points = formatPoints(rawPoints, { x: 1, y: 1 }, pixelToInch) as any
|
|
2622
|
+
|
|
2623
|
+
const lineOpts: any = {
|
|
2624
|
+
x, y, w, h,
|
|
2625
|
+
line: {
|
|
2626
|
+
color: parseColor(el.style?.color || '#475569').color,
|
|
2627
|
+
width: ((el.style?.strokeWidth || 1) as number) / ratioPx2Pt,
|
|
2628
|
+
dashType: el.lineStyle === 'dashed' ? 'dash' : 'solid',
|
|
2629
|
+
beginArrowType: beginArrow as any,
|
|
2630
|
+
endArrowType: endArrow as any
|
|
2631
|
+
},
|
|
2632
|
+
points,
|
|
2633
|
+
rotate, flipH, flipV,
|
|
2634
|
+
objectName: el.id
|
|
2635
|
+
}
|
|
2636
|
+
const lineShadow = el.shadow || (el.style as any)?.shadow
|
|
2637
|
+
let parsedLineShadow = convertElShadowToPptx(lineShadow, ratioPx2Pt)
|
|
2638
|
+
if (!parsedLineShadow && el.style?.boxShadow) {
|
|
2639
|
+
parsedLineShadow = parseCssShadow(el.style.boxShadow, ratioPx2Pt)
|
|
2640
|
+
}
|
|
2641
|
+
if (parsedLineShadow) lineOpts.shadow = parsedLineShadow
|
|
2642
|
+
const lineRefl = el.reflection || el.style?.reflection
|
|
2643
|
+
if (lineRefl) {
|
|
2644
|
+
const parsedReflection = convertElReflectionToPptx(lineRefl, ratioPx2Pt)
|
|
2645
|
+
if (parsedReflection) lineOpts.reflection = parsedReflection
|
|
2646
|
+
}
|
|
2647
|
+
pptxSlide.addShape('custGeom' as any, lineOpts)
|
|
2721
2648
|
}
|
|
2649
|
+
else if (el.type === 'audio' || el.type === 'video') {
|
|
2650
|
+
const mediaInput = (el as any).blob || (el as any).src || el.content || (el as any).value || ''
|
|
2651
|
+
let mediaPath = ''
|
|
2652
|
+
if (typeof Blob !== 'undefined' && mediaInput instanceof Blob) {
|
|
2653
|
+
try {
|
|
2654
|
+
mediaPath = await blobToDataUrl(mediaInput)
|
|
2655
|
+
} catch (e) {
|
|
2656
|
+
console.warn('Converting media Blob to DataURL failed:', e)
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
if (!mediaPath) {
|
|
2660
|
+
mediaPath = await resolveMediaSrc(mediaInput, el.type as 'video' | 'audio')
|
|
2661
|
+
}
|
|
2662
|
+
if (mediaPath && mediaPath.startsWith('data:')) {
|
|
2663
|
+
let extn = el.type === 'audio' ? 'mp3' : 'mp4'
|
|
2664
|
+
if (mediaPath.startsWith('data:audio/')) {
|
|
2665
|
+
if (mediaPath.includes('audio/wav')) extn = 'wav'
|
|
2666
|
+
else if (mediaPath.includes('audio/m4a') || mediaPath.includes('audio/x-m4a') || mediaPath.includes('audio/mp4')) extn = 'm4a'
|
|
2667
|
+
else if (mediaPath.includes('audio/ogg')) extn = 'ogg'
|
|
2668
|
+
else extn = 'mp3'
|
|
2669
|
+
} else if (mediaPath.startsWith('data:video/')) {
|
|
2670
|
+
if (mediaPath.includes('video/webm')) extn = 'webm'
|
|
2671
|
+
else if (mediaPath.includes('video/ogg')) extn = 'ogg'
|
|
2672
|
+
else extn = 'mp4'
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
pptxSlide.addMedia({
|
|
2676
|
+
type: el.type,
|
|
2677
|
+
data: mediaPath,
|
|
2678
|
+
extn,
|
|
2679
|
+
x, y, w, h,
|
|
2680
|
+
objectName: el.id
|
|
2681
|
+
})
|
|
2682
|
+
} else {
|
|
2683
|
+
// 无法获取或跨域拦截二进制数据时的防护兜底:
|
|
2684
|
+
// 严禁调用带 path 的 addMedia,否则 pptxgenjs 会因为 XHR 失败向媒体文件写入损坏的图片数据,导致导出的 PPTX 无法打开!
|
|
2685
|
+
// 生成规范的优雅多媒体占位卡片并挂载在线超链接,确保 PPT 100% 正常打开且可点击跳转播放
|
|
2686
|
+
const isVideo = el.type === 'video'
|
|
2687
|
+
const mediaTitle = isVideo ? '视频' : '音频'
|
|
2688
|
+
const displayUrl = typeof mediaInput === 'string' ? mediaInput : ''
|
|
2689
|
+
|
|
2690
|
+
pptxSlide.addShape('rect' as any, {
|
|
2691
|
+
x, y, w, h,
|
|
2692
|
+
fill: { color: isVideo ? '1E293B' : '0F172A' },
|
|
2693
|
+
line: { color: '3B82F6', width: 1.5, dashType: 'dash' },
|
|
2694
|
+
rectRadius: 0.08,
|
|
2695
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined,
|
|
2696
|
+
objectName: el.id
|
|
2697
|
+
})
|
|
2722
2698
|
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2699
|
+
pptxSlide.addText([
|
|
2700
|
+
{ text: isVideo ? '▶ 视频多媒体' : '🎵 音频多媒体', options: { bold: true, fontSize: 13, color: 'FFFFFF' } },
|
|
2701
|
+
{ text: displayUrl ? `\n(在线链接: ${displayUrl.length > 36 ? displayUrl.slice(0, 36) + '...' : displayUrl})` : '', options: { fontSize: 9, color: '94A3B8' } },
|
|
2702
|
+
{ text: '\n[点击卡片在浏览器中播放]', options: { fontSize: 9, color: '60A5FA' } }
|
|
2703
|
+
], {
|
|
2704
|
+
x, y, w, h,
|
|
2705
|
+
align: 'center',
|
|
2706
|
+
valign: 'middle',
|
|
2707
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined
|
|
2708
|
+
})
|
|
2728
2709
|
}
|
|
2729
2710
|
}
|
|
2711
|
+
else if (el.type === 'table') {
|
|
2712
|
+
const res = (el.content && (el.content.includes('<table') || el.content.includes('<table')))
|
|
2713
|
+
? parseHTMLTable(el.content, w)
|
|
2714
|
+
: parseTableElementData(el, w)
|
|
2715
|
+
if (res.tableData && res.tableData.length > 0) {
|
|
2716
|
+
const tableOpts: any = { x, y, w, h, objectName: el.id }
|
|
2717
|
+
if (res.colW && res.colW.length > 0) {
|
|
2718
|
+
tableOpts.colW = res.colW
|
|
2719
|
+
}
|
|
2720
|
+
pptxSlide.addTable(res.tableData, tableOpts)
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2730
2724
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2725
|
+
const hasNativeMaster = !!(currentZipMeta?.value?.masterFilesMap && Object.keys(currentZipMeta.value.masterFilesMap).length > 0)
|
|
2726
|
+
const masterElements = getEffectiveMasterElementsForSlide(slide, true)
|
|
2727
|
+
if (!hasNativeMaster && masterElements && masterElements.length > 0) {
|
|
2728
|
+
for (const mEl of masterElements) {
|
|
2729
|
+
await processElement(mEl, null)
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const userElements = (slide.elements || []).filter((e: any) => {
|
|
2734
|
+
if (e.isMaster && !e.isPlaceholder && !e.placeholder && !e.id?.startsWith('ph-') && !e.id?.startsWith('ph_') && !e.name?.includes('占位符')) {
|
|
2735
|
+
if (masterElements && masterElements.some((m: any) => m.id === e.id)) {
|
|
2736
|
+
return false
|
|
2736
2737
|
}
|
|
2737
|
-
return true
|
|
2738
|
-
})
|
|
2739
|
-
for (const el of userElements) {
|
|
2740
|
-
await processElement(el, null)
|
|
2741
2738
|
}
|
|
2739
|
+
return true
|
|
2740
|
+
})
|
|
2741
|
+
for (const el of userElements) {
|
|
2742
|
+
await processElement(el, null)
|
|
2743
|
+
}
|
|
2742
2744
|
}
|
|
2743
2745
|
const rawBlob = (await pptx.write({ outputType: 'blob' })) as Blob
|
|
2744
2746
|
const animatedBlob = await injectAnimationToPptxBlob(
|
|
@@ -2791,6 +2793,14 @@ const handleExportCommand = (command: string) => {
|
|
|
2791
2793
|
}
|
|
2792
2794
|
}
|
|
2793
2795
|
|
|
2796
|
+
const handleImportCommand = (command: string) => {
|
|
2797
|
+
if (command === 'pptx') {
|
|
2798
|
+
triggerPPTXImport()
|
|
2799
|
+
} else if (command === 'json') {
|
|
2800
|
+
triggerJSONImport()
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2794
2804
|
function parseColorToRgb(c: string) {
|
|
2795
2805
|
c = c.trim().toLowerCase();
|
|
2796
2806
|
if (c.startsWith('#')) {
|
|
@@ -5767,12 +5777,12 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
5767
5777
|
points: connectorPoints,
|
|
5768
5778
|
elements: isNestedGroup
|
|
5769
5779
|
? (() => {
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5780
|
+
const nestedChildIdMap = new Map<string, any>(siblings)
|
|
5781
|
+
for (const ch of _el.elements || []) {
|
|
5782
|
+
if (ch && ch.id !== undefined) nestedChildIdMap.set(String(ch.id), ch)
|
|
5783
|
+
}
|
|
5784
|
+
return [...(_el.elements || [])].sort((a: any, b: any) => (a.order || 0) - (b.order || 0)).map((gChild: any) => convertChildElement(gChild, curW, curH, nestedChildIdMap))
|
|
5785
|
+
})()
|
|
5776
5786
|
: undefined
|
|
5777
5787
|
}
|
|
5778
5788
|
}
|
|
@@ -5820,6 +5830,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
5820
5830
|
elements: sortedTopGroupChildren.map((child: any) => convertChildElement(child, topW, topH, childIdMap))
|
|
5821
5831
|
}
|
|
5822
5832
|
|
|
5833
|
+
if (grpFlipH || grpFlipV) {
|
|
5834
|
+
applyGroupGeometryFlip(groupElement, grpFlipH, grpFlipV)
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5823
5837
|
slide.elements.push(groupElement)
|
|
5824
5838
|
}
|
|
5825
5839
|
else if (el.type === 'diagram') {
|
|
@@ -6761,186 +6775,268 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
6761
6775
|
})
|
|
6762
6776
|
console.log(json)
|
|
6763
6777
|
|
|
6764
|
-
|
|
6765
|
-
|
|
6778
|
+
let zipMeta: any = null
|
|
6779
|
+
let loadedZip: JSZip | null = null
|
|
6780
|
+
try {
|
|
6781
|
+
const zip = await JSZipLib.loadAsync(buffer)
|
|
6782
|
+
loadedZip = zip
|
|
6783
|
+
const layoutFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideLayouts/slideLayout') && !k.includes('_rels') && !zip.files[k].dir)
|
|
6784
|
+
const sortSlideXml = (p1: string, p2: string) => {
|
|
6785
|
+
const m1 = /(\d+)\.xml/.exec(p1)
|
|
6786
|
+
const m2 = /(\d+)\.xml/.exec(p2)
|
|
6787
|
+
const n1 = m1 ? +m1[1] : 0
|
|
6788
|
+
const n2 = m2 ? +m2[1] : 0
|
|
6789
|
+
return n1 - n2
|
|
6790
|
+
}
|
|
6791
|
+
// 1. 提前解析主题配色 clrScheme(dk1/lt1/dk2/lt2/accent1-6),供母版、版式背景与装饰形状还原真实颜色
|
|
6792
|
+
const themeColors: Record<string, string> = {}
|
|
6766
6793
|
try {
|
|
6767
|
-
const
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
// 1. 提前解析主题配色 clrScheme(dk1/lt1/dk2/lt2/accent1-6),供母版、版式背景与装饰形状还原真实颜色
|
|
6778
|
-
const themeColors: Record<string, string> = {}
|
|
6779
|
-
try {
|
|
6780
|
-
const themeFiles = Object.keys(zip.files).filter(k => /^ppt\/theme\/theme\d+\.xml$/.test(k))
|
|
6781
|
-
if (themeFiles.length > 0) {
|
|
6782
|
-
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
6783
|
-
const schemeM = tTxt.match(/<a:clrScheme[\s\S]*?<\/a:clrScheme>/)
|
|
6784
|
-
if (schemeM) {
|
|
6785
|
-
const slotRe = /<a:(dk1|lt1|dk2|lt2|accent1|accent2|accent3|accent4|accent5|accent6|hlink|folHlink)>([\s\S]*?)<\/a:\1>/g
|
|
6786
|
-
let slotMatch: RegExpExecArray | null
|
|
6787
|
-
while ((slotMatch = slotRe.exec(schemeM[0]))) {
|
|
6788
|
-
const hexM = slotMatch[2].match(/<a:srgbClr val="([0-9A-Fa-f]{6})"/) || slotMatch[2].match(/<a:sysClr[^>]*lastClr="([0-9A-Fa-f]{6})"/)
|
|
6789
|
-
if (hexM) themeColors[slotMatch[1]] = '#' + hexM[1].toUpperCase()
|
|
6790
|
-
}
|
|
6794
|
+
const themeFiles = Object.keys(zip.files).filter(k => /^ppt\/theme\/theme\d+\.xml$/.test(k))
|
|
6795
|
+
if (themeFiles.length > 0) {
|
|
6796
|
+
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
6797
|
+
const schemeM = tTxt.match(/<a:clrScheme[\s\S]*?<\/a:clrScheme>/)
|
|
6798
|
+
if (schemeM) {
|
|
6799
|
+
const slotRe = /<a:(dk1|lt1|dk2|lt2|accent1|accent2|accent3|accent4|accent5|accent6|hlink|folHlink)>([\s\S]*?)<\/a:\1>/g
|
|
6800
|
+
let slotMatch: RegExpExecArray | null
|
|
6801
|
+
while ((slotMatch = slotRe.exec(schemeM[0]))) {
|
|
6802
|
+
const hexM = slotMatch[2].match(/<a:srgbClr val="([0-9A-Fa-f]{6})"/) || slotMatch[2].match(/<a:sysClr[^>]*lastClr="([0-9A-Fa-f]{6})"/)
|
|
6803
|
+
if (hexM) themeColors[slotMatch[1]] = '#' + hexM[1].toUpperCase()
|
|
6791
6804
|
}
|
|
6792
6805
|
}
|
|
6793
|
-
}
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
}
|
|
6806
|
+
}
|
|
6807
|
+
} catch (e) { }
|
|
6808
|
+
|
|
6809
|
+
// 通用背景 XML (<p:bg>) 解析器:全面支持 <a:blip> 图片、<a:gradFill> 渐变(含 circle/radial 径向渐变)、<a:srgbClr> 与 <a:schemeClr> 单色
|
|
6810
|
+
const parsePptxBgFromXml = async (bgTxt: string, xmlPath: string): Promise<string | null> => {
|
|
6811
|
+
if (!bgTxt) return null
|
|
6812
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
6813
|
+
|
|
6814
|
+
// 1. 图片填充 <a:blip>
|
|
6815
|
+
const blipMatch = bgTxt.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)
|
|
6816
|
+
if (blipMatch) {
|
|
6817
|
+
const rId = blipMatch[1]
|
|
6818
|
+
const relsPath = xmlPath.replace(/(slideMasters|slideLayouts)\/([^/]+)\.xml$/, '$1/_rels/$2.xml.rels')
|
|
6819
|
+
const relsTxt = await zip.file(relsPath)?.async('text') || ''
|
|
6820
|
+
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6821
|
+
if (relM) {
|
|
6822
|
+
const stack = xmlPath.split('/').slice(0, -1)
|
|
6823
|
+
for (const part of relM[1].split('/')) {
|
|
6824
|
+
if (part === '..') stack.pop()
|
|
6825
|
+
else if (part && part !== '.') stack.push(part)
|
|
6826
|
+
}
|
|
6827
|
+
const mediaPath = stack.join('/')
|
|
6828
|
+
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6829
|
+
const blob = await zip.file(mediaPath)?.async('blob')
|
|
6830
|
+
if (blob) {
|
|
6831
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
6832
|
+
return `#ffffff url("${blobUrl}") center center / 100% 100% no-repeat`
|
|
6820
6833
|
}
|
|
6821
6834
|
}
|
|
6835
|
+
}
|
|
6822
6836
|
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
}
|
|
6837
|
+
// 2. 渐变填充 <a:gradFill>
|
|
6838
|
+
const gradMatch = bgTxt.match(/<a:gradFill\b[\s\S]*?<\/a:gradFill>/)
|
|
6839
|
+
if (gradMatch) {
|
|
6840
|
+
const gradTxt = gradMatch[0]
|
|
6841
|
+
const gsList = gradTxt.match(/<a:gs\b[\s\S]*?<\/a:gs>/g) || []
|
|
6842
|
+
if (gsList.length > 0) {
|
|
6843
|
+
const stops: string[] = []
|
|
6844
|
+
for (const gs of gsList) {
|
|
6845
|
+
const posM = gs.match(/pos="(\d+)"/)
|
|
6846
|
+
const posVal = posM ? Math.round(parseInt(posM[1], 10) / 1000) : 0
|
|
6847
|
+
let col = '#FFFFFF'
|
|
6848
|
+
const srgbM = gs.match(/<a:srgbClr\s+val="([0-9A-Fa-f]{6})"/)
|
|
6849
|
+
if (srgbM) {
|
|
6850
|
+
col = '#' + srgbM[1].toUpperCase()
|
|
6851
|
+
} else {
|
|
6852
|
+
const schemeM = gs.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || gs.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6853
|
+
if (schemeM) {
|
|
6854
|
+
const val = schemeM[1]
|
|
6855
|
+
const inner = schemeM[2] || ''
|
|
6856
|
+
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6857
|
+
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6858
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6859
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6860
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6861
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6862
|
+
col = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6850
6863
|
}
|
|
6851
|
-
stops.push(`${col} ${posVal}%`)
|
|
6852
6864
|
}
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
return `radial-gradient(closest-side at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6879
|
-
}
|
|
6880
|
-
return `radial-gradient(circle farthest-corner at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6865
|
+
stops.push(`${col} ${posVal}%`)
|
|
6866
|
+
}
|
|
6867
|
+
const pathMatch = gradTxt.match(/<a:path\b[^>]*path="([^"]+)"/i)
|
|
6868
|
+
const pathKind = pathMatch ? pathMatch[1].toLowerCase() : ''
|
|
6869
|
+
const isPath = pathKind === 'shape'
|
|
6870
|
+
const isCircle = pathKind === 'circle' || pathKind === 'radial'
|
|
6871
|
+
const isRect = pathKind === 'rect'
|
|
6872
|
+
const isRadialOrRect = isCircle || isRect || /<a:fillToRect/i.test(gradTxt)
|
|
6873
|
+
|
|
6874
|
+
if (isRadialOrRect) {
|
|
6875
|
+
const fillToRectM = gradTxt.match(/<a:fillToRect\b([^>]*)\/?>/)
|
|
6876
|
+
let cx = 50
|
|
6877
|
+
let cy = 50
|
|
6878
|
+
if (fillToRectM) {
|
|
6879
|
+
const attrs = fillToRectM[1]
|
|
6880
|
+
const lM = attrs.match(/l="(-?\d+)"/)
|
|
6881
|
+
const tM = attrs.match(/t="(-?\d+)"/)
|
|
6882
|
+
const rM = attrs.match(/r="(-?\d+)"/)
|
|
6883
|
+
const bM = attrs.match(/b="(-?\d+)"/)
|
|
6884
|
+
const l = lM ? parseInt(lM[1], 10) / 100000 : 0.5
|
|
6885
|
+
const t = tM ? parseInt(tM[1], 10) / 100000 : 0.5
|
|
6886
|
+
const r = rM ? parseInt(rM[1], 10) / 100000 : 0.5
|
|
6887
|
+
const b = bM ? parseInt(bM[1], 10) / 100000 : 0.5
|
|
6888
|
+
cx = Math.round(((l + (1 - r)) / 2) * 100)
|
|
6889
|
+
cy = Math.round(((t + (1 - b)) / 2) * 100)
|
|
6881
6890
|
}
|
|
6882
|
-
if (
|
|
6883
|
-
return `radial-gradient(
|
|
6891
|
+
if (isRect) {
|
|
6892
|
+
return `radial-gradient(closest-side at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6884
6893
|
}
|
|
6885
|
-
|
|
6886
|
-
const angVal = linM ? parseInt(linM[1], 10) / 60000 : 0
|
|
6887
|
-
const cssAng = (Math.round(angVal) + 90) % 360
|
|
6888
|
-
return `linear-gradient(${cssAng}deg, ${stops.join(', ')})`
|
|
6894
|
+
return `radial-gradient(circle farthest-corner at ${cx}% ${cy}%, ${stops.join(', ')})`
|
|
6889
6895
|
}
|
|
6896
|
+
if (isPath) {
|
|
6897
|
+
return `radial-gradient(ellipse farthest-corner at 50% 50%, ${stops.join(', ')})`
|
|
6898
|
+
}
|
|
6899
|
+
const linM = gradTxt.match(/<a:lin\b[^>]*ang="(-?\d+)"/)
|
|
6900
|
+
const angVal = linM ? parseInt(linM[1], 10) / 60000 : 0
|
|
6901
|
+
const cssAng = (Math.round(angVal) + 90) % 360
|
|
6902
|
+
return `linear-gradient(${cssAng}deg, ${stops.join(', ')})`
|
|
6890
6903
|
}
|
|
6904
|
+
}
|
|
6891
6905
|
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
}
|
|
6897
|
-
const schemeMatch = bgTxt.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || bgTxt.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6898
|
-
if (schemeMatch) {
|
|
6899
|
-
const val = schemeMatch[1]
|
|
6900
|
-
const inner = schemeMatch[2] || ''
|
|
6901
|
-
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6902
|
-
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6903
|
-
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6904
|
-
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6905
|
-
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6906
|
-
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6907
|
-
return (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6908
|
-
}
|
|
6909
|
-
|
|
6910
|
-
return null
|
|
6906
|
+
// 3. 纯色填充 <a:srgbClr> 或 <a:schemeClr>
|
|
6907
|
+
const srgbMatch = bgTxt.match(/<a:srgbClr\s+val="([0-9A-Fa-f]{6})"/)
|
|
6908
|
+
if (srgbMatch) {
|
|
6909
|
+
return '#' + srgbMatch[1].toUpperCase()
|
|
6911
6910
|
}
|
|
6911
|
+
const schemeMatch = bgTxt.match(/<a:schemeClr\s+val="([^"]+)"([\s\S]*?)<\/a:schemeClr>/) || bgTxt.match(/<a:schemeClr\s+val="([^"]+)"\s*\/>/)
|
|
6912
|
+
if (schemeMatch) {
|
|
6913
|
+
const val = schemeMatch[1]
|
|
6914
|
+
const inner = schemeMatch[2] || ''
|
|
6915
|
+
const lumModM = inner.match(/<a:lumMod val="(\d+)"/)
|
|
6916
|
+
const lumOffM = inner.match(/<a:lumOff val="(\d+)"/)
|
|
6917
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
6918
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
6919
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
6920
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
6921
|
+
return (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
return null
|
|
6925
|
+
}
|
|
6926
|
+
|
|
6927
|
+
layoutFiles.sort(sortSlideXml)
|
|
6928
|
+
|
|
6929
|
+
const allLayouts: any[] = []
|
|
6930
|
+
const layoutByFile = new Map<string, any>()
|
|
6931
|
+
|
|
6932
|
+
for (let i = 0; i < layoutFiles.length; i++) {
|
|
6933
|
+
const lf = layoutFiles[i]
|
|
6934
|
+
const txt = await zip.file(lf)?.async('text') || ''
|
|
6935
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
6912
6936
|
|
|
6913
|
-
|
|
6937
|
+
// 解析版式背景 (<p:bg>) 图片、渐变或颜色填充
|
|
6938
|
+
let layoutBg: string | null = null
|
|
6939
|
+
const bgMatch = txt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
6940
|
+
if (bgMatch) {
|
|
6941
|
+
layoutBg = await parsePptxBgFromXml(bgMatch[0], lf)
|
|
6942
|
+
}
|
|
6914
6943
|
|
|
6915
|
-
|
|
6916
|
-
|
|
6944
|
+
// 版式装饰图片(userDrawn <p:pic>,如整幅世界地图):未被任何页使用的版式走
|
|
6945
|
+
// XML 解析分支(parseLayoutShapesFromXml),sp 解析管不到 pic,需经 rels 把
|
|
6946
|
+
// media 转 base64 一并传入,否则该版式在母版编辑视图/缩略图整片空白
|
|
6947
|
+
const picSrcMap: Record<string, string> = {}
|
|
6948
|
+
const layoutPicMatches = txt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
6949
|
+
if (layoutPicMatches.length > 0) {
|
|
6950
|
+
const relsTxt = await zip.file(lf.replace(/(slideLayouts\/)(slideLayout\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
6951
|
+
for (const pic of layoutPicMatches) {
|
|
6952
|
+
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
6953
|
+
if (!rId || picSrcMap[rId]) continue
|
|
6954
|
+
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6955
|
+
if (!relM) continue
|
|
6956
|
+
// target 相对 slideLayouts 目录(如 ../media/image2.jpeg)→ zip 内绝对路径
|
|
6957
|
+
const stack = lf.split('/').slice(0, -1)
|
|
6958
|
+
for (const part of relM[1].split('/')) {
|
|
6959
|
+
if (part === '..') stack.pop()
|
|
6960
|
+
else if (part && part !== '.') stack.push(part)
|
|
6961
|
+
}
|
|
6962
|
+
const mediaPath = stack.join('/')
|
|
6963
|
+
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6964
|
+
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
6965
|
+
if (b64) picSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
6966
|
+
}
|
|
6967
|
+
}
|
|
6968
|
+
const nameM = txt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
6969
|
+
const typeM = txt.match(/<p:sldLayout[^>]*type="([^"]+)"/)
|
|
6970
|
+
const isTitle = (typeM && typeM[1] === 'title') || txt.includes('type="ctrTitle"') || (nameM && (nameM[1].includes('Title') || nameM[1].includes('标题')))
|
|
6971
|
+
const isBlank = (typeM && typeM[1] === 'blank') || (nameM && (nameM[1].includes('Blank') || nameM[1].includes('空白')))
|
|
6972
|
+
const picPhCount = (txt.match(/type="pic"/g) || []).length
|
|
6973
|
+
|
|
6974
|
+
const lObj = {
|
|
6975
|
+
file: lf,
|
|
6976
|
+
id: `layout-${i + 1}`,
|
|
6977
|
+
name: nameM ? nameM[1] : `版式 ${i + 1}`,
|
|
6978
|
+
type: typeM ? typeM[1] : (isTitle ? 'title' : (isBlank ? 'blank' : 'content')),
|
|
6979
|
+
background: layoutBg || undefined,
|
|
6980
|
+
picPhCount,
|
|
6981
|
+
picSrcMap,
|
|
6982
|
+
xml: txt
|
|
6983
|
+
}
|
|
6984
|
+
allLayouts.push(lObj)
|
|
6985
|
+
layoutByFile.set(lf, lObj)
|
|
6986
|
+
const shortName = lf.replace('ppt/slideLayouts/', '')
|
|
6987
|
+
layoutByFile.set(shortName, lObj)
|
|
6988
|
+
layoutByFile.set(`../slideLayouts/${shortName}`, lObj)
|
|
6989
|
+
}
|
|
6917
6990
|
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6991
|
+
const slideRelsFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/_rels/slide') && k.endsWith('.rels'))
|
|
6992
|
+
slideRelsFiles.sort(sortSlideXml)
|
|
6993
|
+
|
|
6994
|
+
const slideLayoutMap: any[] = []
|
|
6995
|
+
const slideLayoutTargetList: string[] = []
|
|
6996
|
+
for (let i = 0; i < slideRelsFiles.length; i++) {
|
|
6997
|
+
const srf = slideRelsFiles[i]
|
|
6998
|
+
const txt = await zip.file(srf)?.async('text') || ''
|
|
6999
|
+
const layoutMatch = txt.match(/Type="[^"]*\/slideLayout"[^>]*Target="([^"]+)"/) || txt.match(/Target="([^"]+)"[^>]*Type="[^"]*\/slideLayout"/)
|
|
7000
|
+
const target = layoutMatch ? layoutMatch[1] : ''
|
|
7001
|
+
slideLayoutTargetList.push(target)
|
|
7002
|
+
const lObj = layoutByFile.get(target)
|
|
7003
|
+
slideLayoutMap.push(lObj || null)
|
|
7004
|
+
}
|
|
6922
7005
|
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
7006
|
+
let masterName = nameWithoutExt
|
|
7007
|
+
const masterShapesInfo: Array<{ name: string; text?: string; isPic?: boolean }> = []
|
|
7008
|
+
// 母版占位符 xfrm 位置表(idx/type → px 盒子):版式占位符常省略自身 xfrm(继承母版),
|
|
7009
|
+
// 解析版式提示框时按此回退,否则这类版式解析不出占位符、缩略图整片空白
|
|
7010
|
+
const masterPhXfrm: Record<string, { left: number; top: number; width: number; height: number }> = {}
|
|
7011
|
+
let primaryMasterXml = ''
|
|
7012
|
+
let masterXmlBg: string | null = null
|
|
7013
|
+
const masterPicSrcMap: Record<string, string> = {}
|
|
7014
|
+
try {
|
|
7015
|
+
const masterFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideMasters/slideMaster') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7016
|
+
masterFiles.sort(sortSlideXml)
|
|
7017
|
+
for (const mf of masterFiles) {
|
|
7018
|
+
const mTxt = await zip.file(mf)?.async('text') || ''
|
|
7019
|
+
if (!primaryMasterXml) primaryMasterXml = mTxt
|
|
7020
|
+
|
|
7021
|
+
// 提取母版背景 (<p:bg>)
|
|
7022
|
+
if (!masterXmlBg) {
|
|
7023
|
+
const mBgMatch = mTxt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
7024
|
+
if (mBgMatch) {
|
|
7025
|
+
masterXmlBg = await parsePptxBgFromXml(mBgMatch[0], mf)
|
|
7026
|
+
}
|
|
6928
7027
|
}
|
|
6929
7028
|
|
|
6930
|
-
//
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
const relsTxt = await zip.file(lf.replace(/(slideLayouts\/)(slideLayout\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
6937
|
-
for (const pic of layoutPicMatches) {
|
|
7029
|
+
// 防御策略 3:从 slideMasters/_rels/slideMaster*.xml.rels 提取母版图片引用,解析为 Base64 供 Logo/背景图显影
|
|
7030
|
+
const masterPicMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7031
|
+
if (masterPicMatches.length > 0) {
|
|
7032
|
+
const relsTxt = await zip.file(mf.replace(/(slideMasters\/)(slideMaster\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
7033
|
+
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
7034
|
+
for (const pic of masterPicMatches) {
|
|
6938
7035
|
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
6939
|
-
if (!rId ||
|
|
7036
|
+
if (!rId || masterPicSrcMap[rId]) continue
|
|
6940
7037
|
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
6941
7038
|
if (!relM) continue
|
|
6942
|
-
|
|
6943
|
-
const stack = lf.split('/').slice(0, -1)
|
|
7039
|
+
const stack = mf.split('/').slice(0, -1)
|
|
6944
7040
|
for (const part of relM[1].split('/')) {
|
|
6945
7041
|
if (part === '..') stack.pop()
|
|
6946
7042
|
else if (part && part !== '.') stack.push(part)
|
|
@@ -6948,481 +7044,399 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
6948
7044
|
const mediaPath = stack.join('/')
|
|
6949
7045
|
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
6950
7046
|
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
6951
|
-
if (b64)
|
|
7047
|
+
if (b64) masterPicSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
6952
7048
|
}
|
|
6953
7049
|
}
|
|
6954
|
-
const nameM = txt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
6955
|
-
const typeM = txt.match(/<p:sldLayout[^>]*type="([^"]+)"/)
|
|
6956
|
-
const isTitle = (typeM && typeM[1] === 'title') || txt.includes('type="ctrTitle"') || (nameM && (nameM[1].includes('Title') || nameM[1].includes('标题')))
|
|
6957
|
-
const isBlank = (typeM && typeM[1] === 'blank') || (nameM && (nameM[1].includes('Blank') || nameM[1].includes('空白')))
|
|
6958
|
-
const picPhCount = (txt.match(/type="pic"/g) || []).length
|
|
6959
|
-
|
|
6960
|
-
const lObj = {
|
|
6961
|
-
file: lf,
|
|
6962
|
-
id: `layout-${i + 1}`,
|
|
6963
|
-
name: nameM ? nameM[1] : `版式 ${i + 1}`,
|
|
6964
|
-
type: typeM ? typeM[1] : (isTitle ? 'title' : (isBlank ? 'blank' : 'content')),
|
|
6965
|
-
background: layoutBg || undefined,
|
|
6966
|
-
picPhCount,
|
|
6967
|
-
picSrcMap,
|
|
6968
|
-
xml: txt
|
|
6969
|
-
}
|
|
6970
|
-
allLayouts.push(lObj)
|
|
6971
|
-
layoutByFile.set(lf, lObj)
|
|
6972
|
-
const shortName = lf.replace('ppt/slideLayouts/', '')
|
|
6973
|
-
layoutByFile.set(shortName, lObj)
|
|
6974
|
-
layoutByFile.set(`../slideLayouts/${shortName}`, lObj)
|
|
6975
|
-
}
|
|
6976
7050
|
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
const masterShapesInfo: Array<{ name: string; text?: string; isPic?: boolean }> = []
|
|
6994
|
-
// 母版占位符 xfrm 位置表(idx/type → px 盒子):版式占位符常省略自身 xfrm(继承母版),
|
|
6995
|
-
// 解析版式提示框时按此回退,否则这类版式解析不出占位符、缩略图整片空白
|
|
6996
|
-
const masterPhXfrm: Record<string, { left: number; top: number; width: number; height: number }> = {}
|
|
6997
|
-
let primaryMasterXml = ''
|
|
6998
|
-
let masterXmlBg: string | null = null
|
|
6999
|
-
const masterPicSrcMap: Record<string, string> = {}
|
|
7000
|
-
try {
|
|
7001
|
-
const masterFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slideMasters/slideMaster') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7002
|
-
masterFiles.sort(sortSlideXml)
|
|
7003
|
-
for (const mf of masterFiles) {
|
|
7004
|
-
const mTxt = await zip.file(mf)?.async('text') || ''
|
|
7005
|
-
if (!primaryMasterXml) primaryMasterXml = mTxt
|
|
7006
|
-
|
|
7007
|
-
// 提取母版背景 (<p:bg>)
|
|
7008
|
-
if (!masterXmlBg) {
|
|
7009
|
-
const mBgMatch = mTxt.match(/<p:bg\b[\s\S]*?<\/p:bg>/)
|
|
7010
|
-
if (mBgMatch) {
|
|
7011
|
-
masterXmlBg = await parsePptxBgFromXml(mBgMatch[0], mf)
|
|
7012
|
-
}
|
|
7013
|
-
}
|
|
7014
|
-
|
|
7015
|
-
// 防御策略 3:从 slideMasters/_rels/slideMaster*.xml.rels 提取母版图片引用,解析为 Base64 供 Logo/背景图显影
|
|
7016
|
-
const masterPicMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7017
|
-
if (masterPicMatches.length > 0) {
|
|
7018
|
-
const relsTxt = await zip.file(mf.replace(/(slideMasters\/)(slideMaster\d+\.xml)$/, '$1_rels/$2.rels'))?.async('text') || ''
|
|
7019
|
-
const mimeByExt: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp', svg: 'image/svg+xml', tif: 'image/tiff', tiff: 'image/tiff' }
|
|
7020
|
-
for (const pic of masterPicMatches) {
|
|
7021
|
-
const rId = (pic.match(/r:embed="([^"]+)"/) || [])[1]
|
|
7022
|
-
if (!rId || masterPicSrcMap[rId]) continue
|
|
7023
|
-
const relM = relsTxt.match(new RegExp(`Id="${rId}"[^>]*Target="([^"]+)"`)) || relsTxt.match(new RegExp(`Target="([^"]+)"[^>]*Id="${rId}"`))
|
|
7024
|
-
if (!relM) continue
|
|
7025
|
-
const stack = mf.split('/').slice(0, -1)
|
|
7026
|
-
for (const part of relM[1].split('/')) {
|
|
7027
|
-
if (part === '..') stack.pop()
|
|
7028
|
-
else if (part && part !== '.') stack.push(part)
|
|
7029
|
-
}
|
|
7030
|
-
const mediaPath = stack.join('/')
|
|
7031
|
-
const ext = ((mediaPath.match(/\.(\w+)$/) || [])[1] || '').toLowerCase()
|
|
7032
|
-
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7033
|
-
if (b64) masterPicSrcMap[rId] = `data:${mimeByExt[ext] || 'application/octet-stream'};base64,${b64}`
|
|
7034
|
-
}
|
|
7035
|
-
}
|
|
7036
|
-
|
|
7037
|
-
const spMatches = mTxt.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7038
|
-
for (const sp of spMatches) {
|
|
7039
|
-
const name = sp.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || sp.match(/name="([^"]+)"/)?.[1] || ''
|
|
7040
|
-
const text = (sp.match(/<a:t>([^<]+)<\/a:t>/g) || []).map(t => t.replace(/<\/?a:t>/g, '')).join('').trim()
|
|
7041
|
-
masterShapesInfo.push({ name, text })
|
|
7042
|
-
const phAttrsM = sp.match(/<p:ph\b([^>]*)\/?>/)
|
|
7043
|
-
if (phAttrsM) {
|
|
7044
|
-
const phAttrs = phAttrsM[1]
|
|
7045
|
-
const idxAttr = (phAttrs.match(/idx="(\d+)"/) || [])[1]
|
|
7046
|
-
const typeAttr = (phAttrs.match(/type="(\w+)"/) || [])[1]
|
|
7047
|
-
const off = sp.match(/<a:off\s+x="(-?\d+)"\s+y="(-?\d+)"/)
|
|
7048
|
-
const ext = sp.match(/<a:ext\s+cx="(\d+)"\s+cy="(\d+)"/)
|
|
7049
|
-
if (off && ext) {
|
|
7050
|
-
const box = { left: Math.round(parseInt(off[1]) / 9525), top: Math.round(parseInt(off[2]) / 9525), width: Math.round(parseInt(ext[1]) / 9525), height: Math.round(parseInt(ext[2]) / 9525) }
|
|
7051
|
-
if (idxAttr !== undefined && !masterPhXfrm['idx_' + idxAttr]) masterPhXfrm['idx_' + idxAttr] = box
|
|
7052
|
-
if (typeAttr && !masterPhXfrm['type_' + typeAttr]) masterPhXfrm['type_' + typeAttr] = box
|
|
7053
|
-
}
|
|
7051
|
+
const spMatches = mTxt.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7052
|
+
for (const sp of spMatches) {
|
|
7053
|
+
const name = sp.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || sp.match(/name="([^"]+)"/)?.[1] || ''
|
|
7054
|
+
const text = (sp.match(/<a:t>([^<]+)<\/a:t>/g) || []).map(t => t.replace(/<\/?a:t>/g, '')).join('').trim()
|
|
7055
|
+
masterShapesInfo.push({ name, text })
|
|
7056
|
+
const phAttrsM = sp.match(/<p:ph\b([^>]*)\/?>/)
|
|
7057
|
+
if (phAttrsM) {
|
|
7058
|
+
const phAttrs = phAttrsM[1]
|
|
7059
|
+
const idxAttr = (phAttrs.match(/idx="(\d+)"/) || [])[1]
|
|
7060
|
+
const typeAttr = (phAttrs.match(/type="(\w+)"/) || [])[1]
|
|
7061
|
+
const off = sp.match(/<a:off\s+x="(-?\d+)"\s+y="(-?\d+)"/)
|
|
7062
|
+
const ext = sp.match(/<a:ext\s+cx="(\d+)"\s+cy="(\d+)"/)
|
|
7063
|
+
if (off && ext) {
|
|
7064
|
+
const box = { left: Math.round(parseInt(off[1]) / 9525), top: Math.round(parseInt(off[2]) / 9525), width: Math.round(parseInt(ext[1]) / 9525), height: Math.round(parseInt(ext[2]) / 9525) }
|
|
7065
|
+
if (idxAttr !== undefined && !masterPhXfrm['idx_' + idxAttr]) masterPhXfrm['idx_' + idxAttr] = box
|
|
7066
|
+
if (typeAttr && !masterPhXfrm['type_' + typeAttr]) masterPhXfrm['type_' + typeAttr] = box
|
|
7054
7067
|
}
|
|
7055
7068
|
}
|
|
7056
|
-
const picMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7057
|
-
for (const pic of picMatches) {
|
|
7058
|
-
const name = pic.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || pic.match(/name="([^"]+)"/)?.[1] || ''
|
|
7059
|
-
masterShapesInfo.push({ name, isPic: true })
|
|
7060
|
-
}
|
|
7061
7069
|
}
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
const
|
|
7065
|
-
|
|
7070
|
+
const picMatches = mTxt.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7071
|
+
for (const pic of picMatches) {
|
|
7072
|
+
const name = pic.match(/<p:cNvPr[^>]*name="([^"]+)"/)?.[1] || pic.match(/name="([^"]+)"/)?.[1] || ''
|
|
7073
|
+
masterShapesInfo.push({ name, isPic: true })
|
|
7066
7074
|
}
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
+
}
|
|
7076
|
+
if (masterFiles.length > 0) {
|
|
7077
|
+
const mTxt = await zip.file(masterFiles[0])?.async('text') || ''
|
|
7078
|
+
const mNameM = mTxt.match(/<p:cSld[^>]*name="([^"]+)"/)
|
|
7079
|
+
if (mNameM && mNameM[1]) masterName = mNameM[1]
|
|
7080
|
+
}
|
|
7081
|
+
if (!masterName || masterName === nameWithoutExt) {
|
|
7082
|
+
const themeFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/theme/theme') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7083
|
+
if (themeFiles.length > 0) {
|
|
7084
|
+
const tTxt = await zip.file(themeFiles[0])?.async('text') || ''
|
|
7085
|
+
const tNameM = tTxt.match(/<a:theme[^>]*name="([^"]+)"/)
|
|
7086
|
+
if (tNameM && tNameM[1] && tNameM[1] !== 'Office 主题' && tNameM[1] !== 'Office Theme') {
|
|
7087
|
+
masterName = tNameM[1]
|
|
7075
7088
|
}
|
|
7076
7089
|
}
|
|
7077
|
-
}
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7090
|
+
}
|
|
7091
|
+
} catch (e) { }
|
|
7092
|
+
|
|
7093
|
+
|
|
7094
|
+
// 从母版 primaryMasterXml 解析标准文本色(txStyles: titleStyle/bodyStyle),支持 lumMod/lumOff 调制
|
|
7095
|
+
let masterTextColor = ''
|
|
7096
|
+
try {
|
|
7097
|
+
if (primaryMasterXml) {
|
|
7098
|
+
const titleMatch = primaryMasterXml.match(/<p:titleStyle>[\s\S]*?<a:schemeClr val="([^"]+)">([\s\S]*?)<\/a:schemeClr>/)
|
|
7099
|
+
const bodyMatch = primaryMasterXml.match(/<p:bodyStyle>[\s\S]*?<a:schemeClr val="([^"]+)">([\s\S]*?)<\/a:schemeClr>/)
|
|
7100
|
+
const m = bodyMatch || titleMatch
|
|
7101
|
+
if (m) {
|
|
7102
|
+
const val = m[1]
|
|
7103
|
+
const lumModM = m[2].match(/<a:lumMod val="(\d+)"/)
|
|
7104
|
+
const lumOffM = m[2].match(/<a:lumOff val="(\d+)"/)
|
|
7105
|
+
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7106
|
+
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7107
|
+
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7108
|
+
const baseHex = themeColors[slot] || themeColors[val] || (slot === 'dk1' || slot === 'tx1' ? '#000000' : '#FFFFFF')
|
|
7109
|
+
masterTextColor = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
} catch (mErr) { }
|
|
7113
|
+
|
|
7114
|
+
// 提取并扫描全局 schemeClr 表达片段与计算颜色映射(仅针对幻灯片、母版、版式与主题核心XML)
|
|
7115
|
+
const schemeFillMap: Record<string, string> = {}
|
|
7116
|
+
try {
|
|
7117
|
+
for (const [name, file] of Object.entries(zip.files)) {
|
|
7118
|
+
if (name.endsWith('.xml') && !name.includes('_rels') && (name.startsWith('ppt/slides/') || name.startsWith('ppt/slideMasters/') || name.startsWith('ppt/slideLayouts/') || name.startsWith('ppt/theme/'))) {
|
|
7119
|
+
const txt = await file.async('text')
|
|
7120
|
+
const regex = /<a:schemeClr\b[^>]*\/>|<a:schemeClr\b[^>]*>(?:(?!<\/a:schemeClr>)[\s\S])*<\/a:schemeClr>/g
|
|
7121
|
+
const matches = txt.match(regex) || []
|
|
7122
|
+
for (const m of matches) {
|
|
7123
|
+
const clean = m.replace(/\s+/g, ' ').trim()
|
|
7124
|
+
const valM = clean.match(/val="([^"]+)"/)
|
|
7125
|
+
if (!valM) continue
|
|
7126
|
+
const val = valM[1]
|
|
7127
|
+
if (val === 'phClr') continue
|
|
7128
|
+
const lumModM = clean.match(/<a:lumMod val="(\d+)"/)
|
|
7129
|
+
const lumOffM = clean.match(/<a:lumOff val="(\d+)"/)
|
|
7091
7130
|
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7092
7131
|
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7093
7132
|
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7094
|
-
const baseHex = themeColors[slot] || themeColors[val] ||
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
if (!valM) continue
|
|
7112
|
-
const val = valM[1]
|
|
7113
|
-
if (val === 'phClr') continue
|
|
7114
|
-
const lumModM = clean.match(/<a:lumMod val="(\d+)"/)
|
|
7115
|
-
const lumOffM = clean.match(/<a:lumOff val="(\d+)"/)
|
|
7116
|
-
const lumMod = lumModM ? parseInt(lumModM[1], 10) / 100000 : 1
|
|
7117
|
-
const lumOff = lumOffM ? parseInt(lumOffM[1], 10) / 100000 : 0
|
|
7118
|
-
const slot = val === 'tx1' ? 'dk1' : val === 'bg1' ? 'lt1' : val === 'tx2' ? 'dk2' : val === 'bg2' ? 'lt2' : val
|
|
7119
|
-
const baseHex = themeColors[slot] || themeColors[val] || ''
|
|
7120
|
-
if (baseHex) {
|
|
7121
|
-
const calcHex = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7122
|
-
const cleanHex = calcHex.replace('#', '').toUpperCase()
|
|
7123
|
-
schemeFillMap[cleanHex] = clean
|
|
7124
|
-
|
|
7125
|
-
// 处理 RGB 整数四舍五入 ±1 的容差 (如 8496B0 vs 8497B0)
|
|
7126
|
-
const r = parseInt(cleanHex.slice(0, 2), 16)
|
|
7127
|
-
const g = parseInt(cleanHex.slice(2, 4), 16)
|
|
7128
|
-
const b = parseInt(cleanHex.slice(4, 6), 16)
|
|
7129
|
-
const toHex = (num: number) => num.toString(16).padStart(2, '0').toUpperCase()
|
|
7130
|
-
for (let dg = -1; dg <= 1; dg++) {
|
|
7131
|
-
for (let db = -1; db <= 1; db++) {
|
|
7132
|
-
if (dg === 0 && db === 0) continue
|
|
7133
|
-
const nearHex = `${toHex(r)}${toHex(Math.min(255, Math.max(0, g + dg)))}${toHex(Math.min(255, Math.max(0, b + db)))}`
|
|
7134
|
-
if (!schemeFillMap[nearHex]) {
|
|
7135
|
-
schemeFillMap[nearHex] = clean
|
|
7136
|
-
}
|
|
7133
|
+
const baseHex = themeColors[slot] || themeColors[val] || ''
|
|
7134
|
+
if (baseHex) {
|
|
7135
|
+
const calcHex = (lumMod !== 1 || lumOff !== 0) ? applyLumAdjust(baseHex, lumMod, lumOff) : baseHex
|
|
7136
|
+
const cleanHex = calcHex.replace('#', '').toUpperCase()
|
|
7137
|
+
schemeFillMap[cleanHex] = clean
|
|
7138
|
+
|
|
7139
|
+
// 处理 RGB 整数四舍五入 ±1 的容差 (如 8496B0 vs 8497B0)
|
|
7140
|
+
const r = parseInt(cleanHex.slice(0, 2), 16)
|
|
7141
|
+
const g = parseInt(cleanHex.slice(2, 4), 16)
|
|
7142
|
+
const b = parseInt(cleanHex.slice(4, 6), 16)
|
|
7143
|
+
const toHex = (num: number) => num.toString(16).padStart(2, '0').toUpperCase()
|
|
7144
|
+
for (let dg = -1; dg <= 1; dg++) {
|
|
7145
|
+
for (let db = -1; db <= 1; db++) {
|
|
7146
|
+
if (dg === 0 && db === 0) continue
|
|
7147
|
+
const nearHex = `${toHex(r)}${toHex(Math.min(255, Math.max(0, g + dg)))}${toHex(Math.min(255, Math.max(0, b + db)))}`
|
|
7148
|
+
if (!schemeFillMap[nearHex]) {
|
|
7149
|
+
schemeFillMap[nearHex] = clean
|
|
7137
7150
|
}
|
|
7138
7151
|
}
|
|
7139
7152
|
}
|
|
7140
7153
|
}
|
|
7141
7154
|
}
|
|
7142
7155
|
}
|
|
7143
|
-
}
|
|
7144
|
-
|
|
7145
|
-
// 提取版式占位符几何字典与每页占位符图片的几何形状映射
|
|
7146
|
-
const slidePicGeomMap: Array<Record<string, { geom?: string; polygonPoints?: string }>> = []
|
|
7147
|
-
const slideSpLstStyleAlignMap: Array<Record<string, string>> = []
|
|
7148
|
-
try {
|
|
7149
|
-
const layoutGeomMap = new Map<string, Map<string, { geom?: string; polygonPoints?: string; type?: string }>>()
|
|
7150
|
-
for (const layout of allLayouts) {
|
|
7151
|
-
const phMap = new Map<string, { geom?: string; polygonPoints?: string; type?: string }>()
|
|
7152
|
-
const spMatches = (layout.xml || '').match(/<p:(?:sp|pic)\b[\s\S]*?<\/p:(?:sp|pic)>/g) || []
|
|
7153
|
-
for (const sp of spMatches) {
|
|
7154
|
-
const phMatch = sp.match(/<p:ph\b[^>]*\/?>/)
|
|
7155
|
-
if (!phMatch) continue
|
|
7156
|
-
const phAttrs = phMatch[0]
|
|
7157
|
-
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7158
|
-
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7159
|
-
const prstM = sp.match(/<a:prstGeom\b[^>]*prst="([^"]+)"/)
|
|
7160
|
-
const custM = sp.match(/<a:custGeom\b[\s\S]*?<\/a:custGeom>/)
|
|
7161
|
-
let geom = prstM ? prstM[1] : undefined
|
|
7162
|
-
let polygonPoints: string | undefined = undefined
|
|
7163
|
-
if (custM) {
|
|
7164
|
-
const poly = parseCustGeomToPolygon(custM[0])
|
|
7165
|
-
if (poly) {
|
|
7166
|
-
polygonPoints = poly
|
|
7167
|
-
geom = 'custom'
|
|
7168
|
-
}
|
|
7169
|
-
}
|
|
7170
|
-
const geomInfo = { geom, polygonPoints, type: typeM ? typeM[1] : undefined }
|
|
7171
|
-
if (idxM) phMap.set('idx_' + idxM[1], geomInfo)
|
|
7172
|
-
if (typeM) phMap.set('type_' + typeM[1], geomInfo)
|
|
7173
|
-
}
|
|
7174
|
-
layoutGeomMap.set(layout.file, phMap)
|
|
7175
|
-
const shortName = layout.file.replace('ppt/slideLayouts/', '')
|
|
7176
|
-
layoutGeomMap.set(shortName, phMap)
|
|
7177
|
-
layoutGeomMap.set(`../slideLayouts/${shortName}`, phMap)
|
|
7178
|
-
}
|
|
7156
|
+
}
|
|
7157
|
+
} catch (e) { }
|
|
7179
7158
|
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
const
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
spLstStyleAlignById[id] = standardAlign
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
}
|
|
7207
|
-
slideSpLstStyleAlignMap.push(spLstStyleAlignById)
|
|
7208
|
-
|
|
7209
|
-
const picMatches = sXml.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7210
|
-
for (const pic of picMatches) {
|
|
7211
|
-
const idM = pic.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7212
|
-
if (!idM) continue
|
|
7213
|
-
const id = idM[1]
|
|
7214
|
-
const phMatch = pic.match(/<p:ph\b[^>]*\/?>/)
|
|
7215
|
-
if (phMatch && phMap) {
|
|
7216
|
-
const phAttrs = phMatch[0]
|
|
7217
|
-
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7218
|
-
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7219
|
-
let geomInfo: { geom?: string; polygonPoints?: string } | undefined
|
|
7220
|
-
if (idxM && phMap.has('idx_' + idxM[1])) {
|
|
7221
|
-
geomInfo = phMap.get('idx_' + idxM[1])
|
|
7222
|
-
} else if (typeM && phMap.has('type_' + typeM[1])) {
|
|
7223
|
-
geomInfo = phMap.get('type_' + typeM[1])
|
|
7224
|
-
}
|
|
7225
|
-
if (geomInfo) {
|
|
7226
|
-
picGeomById[id] = geomInfo
|
|
7227
|
-
}
|
|
7159
|
+
// 提取版式占位符几何字典与每页占位符图片的几何形状映射
|
|
7160
|
+
const slidePicGeomMap: Array<Record<string, { geom?: string; polygonPoints?: string }>> = []
|
|
7161
|
+
const slideSpLstStyleAlignMap: Array<Record<string, string>> = []
|
|
7162
|
+
try {
|
|
7163
|
+
const layoutGeomMap = new Map<string, Map<string, { geom?: string; polygonPoints?: string; type?: string }>>()
|
|
7164
|
+
for (const layout of allLayouts) {
|
|
7165
|
+
const phMap = new Map<string, { geom?: string; polygonPoints?: string; type?: string }>()
|
|
7166
|
+
const spMatches = (layout.xml || '').match(/<p:(?:sp|pic)\b[\s\S]*?<\/p:(?:sp|pic)>/g) || []
|
|
7167
|
+
for (const sp of spMatches) {
|
|
7168
|
+
const phMatch = sp.match(/<p:ph\b[^>]*\/?>/)
|
|
7169
|
+
if (!phMatch) continue
|
|
7170
|
+
const phAttrs = phMatch[0]
|
|
7171
|
+
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7172
|
+
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7173
|
+
const prstM = sp.match(/<a:prstGeom\b[^>]*prst="([^"]+)"/)
|
|
7174
|
+
const custM = sp.match(/<a:custGeom\b[\s\S]*?<\/a:custGeom>/)
|
|
7175
|
+
let geom = prstM ? prstM[1] : undefined
|
|
7176
|
+
let polygonPoints: string | undefined = undefined
|
|
7177
|
+
if (custM) {
|
|
7178
|
+
const poly = parseCustGeomToPolygon(custM[0])
|
|
7179
|
+
if (poly) {
|
|
7180
|
+
polygonPoints = poly
|
|
7181
|
+
geom = 'custom'
|
|
7228
7182
|
}
|
|
7229
7183
|
}
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
console.warn('提取占位符几何失败:', geomErr)
|
|
7234
|
-
}
|
|
7235
|
-
|
|
7236
|
-
// 捕获完整原生母版与版式文件及底层依赖媒体,供导出时无损注入原生 PPT 母版架构体系
|
|
7237
|
-
const masterFilesMap: Record<string, string> = {}
|
|
7238
|
-
for (const k of Object.keys(zip.files)) {
|
|
7239
|
-
if (k.startsWith('ppt/slideMasters/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7240
|
-
masterFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7184
|
+
const geomInfo = { geom, polygonPoints, type: typeM ? typeM[1] : undefined }
|
|
7185
|
+
if (idxM) phMap.set('idx_' + idxM[1], geomInfo)
|
|
7186
|
+
if (typeM) phMap.set('type_' + typeM[1], geomInfo)
|
|
7241
7187
|
}
|
|
7188
|
+
layoutGeomMap.set(layout.file, phMap)
|
|
7189
|
+
const shortName = layout.file.replace('ppt/slideLayouts/', '')
|
|
7190
|
+
layoutGeomMap.set(shortName, phMap)
|
|
7191
|
+
layoutGeomMap.set(`../slideLayouts/${shortName}`, phMap)
|
|
7242
7192
|
}
|
|
7243
7193
|
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7194
|
+
const slideFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/slide') && !k.includes('_rels') && !zip.files[k].dir)
|
|
7195
|
+
slideFiles.sort(sortSlideXml)
|
|
7196
|
+
|
|
7197
|
+
for (let i = 0; i < slideFiles.length; i++) {
|
|
7198
|
+
const sXml = await zip.file(slideFiles[i])?.async('text') || ''
|
|
7199
|
+
const lObj = slideLayoutMap[i]
|
|
7200
|
+
const phMap = lObj ? (layoutGeomMap.get(lObj.file) || layoutGeomMap.get(lObj.id)) : null
|
|
7201
|
+
const picGeomById: Record<string, { geom?: string; polygonPoints?: string }> = {}
|
|
7202
|
+
|
|
7203
|
+
// 提取各文本框在 a:lstStyle 中定义的继承对齐方式(当段落自身无显式 a:pPr algn 时兜底生效)
|
|
7204
|
+
const spLstStyleAlignById: Record<string, string> = {}
|
|
7205
|
+
const spMatches = sXml.match(/<p:sp\b[\s\S]*?<\/p:sp>/g) || []
|
|
7206
|
+
for (const sp of spMatches) {
|
|
7207
|
+
const idM = sp.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7208
|
+
if (!idM) continue
|
|
7209
|
+
const id = idM[1]
|
|
7210
|
+
const hasDirectPAlign = /<a:pPr\b[^>]*\balgn="/.test(sp)
|
|
7211
|
+
const lstStyleM = sp.match(/<a:lstStyle\b[\s\S]*?<\/a:lstStyle>/)
|
|
7212
|
+
if (!hasDirectPAlign && lstStyleM) {
|
|
7213
|
+
const algnM = lstStyleM[0].match(/algn="(ctr|center|r|right|l|left|just|dist)"/)
|
|
7214
|
+
if (algnM) {
|
|
7215
|
+
const a = algnM[1]
|
|
7216
|
+
const standardAlign = (a === 'ctr' || a === 'center') ? 'center' : (a === 'r' || a === 'right') ? 'right' : (a === 'dist') ? 'distribute' : (a === 'just') ? 'justify' : 'left'
|
|
7217
|
+
spLstStyleAlignById[id] = standardAlign
|
|
7218
|
+
}
|
|
7219
|
+
}
|
|
7248
7220
|
}
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
const
|
|
7260
|
-
const
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7221
|
+
slideSpLstStyleAlignMap.push(spLstStyleAlignById)
|
|
7222
|
+
|
|
7223
|
+
const picMatches = sXml.match(/<p:pic\b[\s\S]*?<\/p:pic>/g) || []
|
|
7224
|
+
for (const pic of picMatches) {
|
|
7225
|
+
const idM = pic.match(/<p:cNvPr\b[^>]*id="(\d+)"/)
|
|
7226
|
+
if (!idM) continue
|
|
7227
|
+
const id = idM[1]
|
|
7228
|
+
const phMatch = pic.match(/<p:ph\b[^>]*\/?>/)
|
|
7229
|
+
if (phMatch && phMap) {
|
|
7230
|
+
const phAttrs = phMatch[0]
|
|
7231
|
+
const idxM = phAttrs.match(/idx="(\d+)"/)
|
|
7232
|
+
const typeM = phAttrs.match(/type="(\w+)"/)
|
|
7233
|
+
let geomInfo: { geom?: string; polygonPoints?: string } | undefined
|
|
7234
|
+
if (idxM && phMap.has('idx_' + idxM[1])) {
|
|
7235
|
+
geomInfo = phMap.get('idx_' + idxM[1])
|
|
7236
|
+
} else if (typeM && phMap.has('type_' + typeM[1])) {
|
|
7237
|
+
geomInfo = phMap.get('type_' + typeM[1])
|
|
7264
7238
|
}
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7268
|
-
if (b64) masterMediaMap[mediaPath] = b64
|
|
7239
|
+
if (geomInfo) {
|
|
7240
|
+
picGeomById[id] = geomInfo
|
|
7269
7241
|
}
|
|
7270
7242
|
}
|
|
7271
7243
|
}
|
|
7244
|
+
slidePicGeomMap.push(picGeomById)
|
|
7272
7245
|
}
|
|
7246
|
+
} catch (geomErr) {
|
|
7247
|
+
console.warn('提取占位符几何失败:', geomErr)
|
|
7248
|
+
}
|
|
7273
7249
|
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7250
|
+
// 捕获完整原生母版与版式文件及底层依赖媒体,供导出时无损注入原生 PPT 母版架构体系
|
|
7251
|
+
const masterFilesMap: Record<string, string> = {}
|
|
7252
|
+
for (const k of Object.keys(zip.files)) {
|
|
7253
|
+
if (k.startsWith('ppt/slideMasters/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7254
|
+
masterFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7279
7255
|
}
|
|
7256
|
+
}
|
|
7280
7257
|
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
masterShapesInfo,
|
|
7286
|
-
themeColors,
|
|
7287
|
-
schemeFillMap,
|
|
7288
|
-
masterPhXfrm,
|
|
7289
|
-
primaryMasterXml,
|
|
7290
|
-
masterPicSrcMap,
|
|
7291
|
-
masterXmlBg,
|
|
7292
|
-
slidePicGeomMap,
|
|
7293
|
-
slideSpLstStyleAlignMap,
|
|
7294
|
-
masterTextColor,
|
|
7295
|
-
masterFilesMap,
|
|
7296
|
-
layoutFilesMap,
|
|
7297
|
-
masterMediaMap,
|
|
7298
|
-
tagFilesMap,
|
|
7299
|
-
slideLayoutTargetList
|
|
7258
|
+
const layoutFilesMap: Record<string, string> = {}
|
|
7259
|
+
for (const k of Object.keys(zip.files)) {
|
|
7260
|
+
if (k.startsWith('ppt/slideLayouts/') && (k.endsWith('.xml') || k.endsWith('.rels')) && !zip.files[k].dir) {
|
|
7261
|
+
layoutFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7300
7262
|
}
|
|
7301
|
-
} catch (e) {
|
|
7302
|
-
console.warn('Failed to extract zipMeta:', e)
|
|
7303
7263
|
}
|
|
7304
7264
|
|
|
7305
|
-
const
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7265
|
+
const masterMediaMap: Record<string, string> = {}
|
|
7266
|
+
const relsFiles = [...Object.keys(masterFilesMap), ...Object.keys(layoutFilesMap)].filter(k => k.includes('_rels/'))
|
|
7267
|
+
for (const rf of relsFiles) {
|
|
7268
|
+
const rTxt = masterFilesMap[rf] || layoutFilesMap[rf] || ''
|
|
7269
|
+
const targetMatches = rTxt.match(/Target="([^"]+)"/g) || []
|
|
7270
|
+
for (const tm of targetMatches) {
|
|
7271
|
+
const rawTarget = tm.replace(/^Target="/, '').replace(/"$/, '')
|
|
7272
|
+
if (rawTarget.includes('media/') || rawTarget.includes('../media/')) {
|
|
7273
|
+
const sourcePart = rf.replace('/_rels/', '/').replace(/\.rels$/, '')
|
|
7274
|
+
const stack = sourcePart.split('/').slice(0, -1)
|
|
7275
|
+
for (const part of rawTarget.split('/')) {
|
|
7276
|
+
if (part === '..') stack.pop()
|
|
7277
|
+
else if (part && part !== '.') stack.push(part)
|
|
7278
|
+
}
|
|
7279
|
+
const mediaPath = stack.join('/')
|
|
7280
|
+
if (!masterMediaMap[mediaPath] && zip.files[mediaPath]) {
|
|
7281
|
+
const b64 = await zip.file(mediaPath)?.async('base64')
|
|
7282
|
+
if (b64) masterMediaMap[mediaPath] = b64
|
|
7283
|
+
}
|
|
7316
7284
|
}
|
|
7317
7285
|
}
|
|
7318
7286
|
}
|
|
7319
7287
|
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
await
|
|
7288
|
+
const tagFilesMap: Record<string, string> = {}
|
|
7289
|
+
for (const k of Object.keys(zip.files)) {
|
|
7290
|
+
if (k.startsWith('ppt/tags/') && k.endsWith('.xml') && !zip.files[k].dir) {
|
|
7291
|
+
tagFilesMap[k] = await zip.file(k)?.async('text') || ''
|
|
7324
7292
|
}
|
|
7325
|
-
presentationTitle.value = nameWithoutExt
|
|
7326
|
-
importSlides(parsedData)
|
|
7327
|
-
return true
|
|
7328
|
-
} else {
|
|
7329
|
-
notify('导入失败:文件中未发现有效幻灯片页')
|
|
7330
|
-
return false
|
|
7331
7293
|
}
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7294
|
+
|
|
7295
|
+
zipMeta = {
|
|
7296
|
+
allLayouts,
|
|
7297
|
+
slideLayoutMap,
|
|
7298
|
+
masterName: masterName || nameWithoutExt,
|
|
7299
|
+
masterShapesInfo,
|
|
7300
|
+
themeColors,
|
|
7301
|
+
schemeFillMap,
|
|
7302
|
+
masterPhXfrm,
|
|
7303
|
+
primaryMasterXml,
|
|
7304
|
+
masterPicSrcMap,
|
|
7305
|
+
masterXmlBg,
|
|
7306
|
+
slidePicGeomMap,
|
|
7307
|
+
slideSpLstStyleAlignMap,
|
|
7308
|
+
masterTextColor,
|
|
7309
|
+
masterFilesMap,
|
|
7310
|
+
layoutFilesMap,
|
|
7311
|
+
masterMediaMap,
|
|
7312
|
+
tagFilesMap,
|
|
7313
|
+
slideLayoutTargetList
|
|
7314
|
+
}
|
|
7315
|
+
} catch (e) {
|
|
7316
|
+
console.warn('Failed to extract zipMeta:', e)
|
|
7338
7317
|
}
|
|
7339
|
-
}
|
|
7340
7318
|
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7319
|
+
const parsedData = parsePPTXJSONData(json, zipMeta)
|
|
7320
|
+
let animsPerSlide: any[] = []
|
|
7321
|
+
try {
|
|
7322
|
+
animsPerSlide = await extractAnimationsFromPptxArrayBuffer(loadedZip || buffer, parsedData?.slides || [])
|
|
7323
|
+
} catch (animErr) {
|
|
7324
|
+
console.warn('提取 PPTX 动画失败:', animErr)
|
|
7325
|
+
}
|
|
7326
|
+
for (let i = 0; i < (parsedData?.slides?.length || 0); i++) {
|
|
7327
|
+
if (!parsedData.slides[i].animations || !parsedData.slides[i].animations.length) {
|
|
7328
|
+
if (animsPerSlide && animsPerSlide[i] && animsPerSlide[i].length) {
|
|
7329
|
+
parsedData.slides[i].animations = animsPerSlide[i]
|
|
7330
|
+
}
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7345
7333
|
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
const buffer = event.target?.result as ArrayBuffer
|
|
7351
|
-
await importPptxArrayBuffer(buffer, file.name)
|
|
7352
|
-
} finally {
|
|
7353
|
-
target.value = ''
|
|
7334
|
+
if (parsedData.slides.length > 0) {
|
|
7335
|
+
if (typeof getCustomUploadHandler === 'function' && getCustomUploadHandler()) {
|
|
7336
|
+
loadingText.value = '正在上传媒体资源并导入...'
|
|
7337
|
+
await uploadMediaResourcesInSlides(parsedData)
|
|
7354
7338
|
}
|
|
7339
|
+
presentationTitle.value = nameWithoutExt
|
|
7340
|
+
importSlides(parsedData)
|
|
7341
|
+
return true
|
|
7342
|
+
} else {
|
|
7343
|
+
notify('导入失败:文件中未发现有效幻灯片页')
|
|
7344
|
+
return false
|
|
7355
7345
|
}
|
|
7356
|
-
|
|
7346
|
+
} catch (err: any) {
|
|
7347
|
+
console.error(err)
|
|
7348
|
+
notify('导入 PPTX 失败: ' + err.message)
|
|
7349
|
+
return false
|
|
7350
|
+
} finally {
|
|
7351
|
+
isExporting.value = false
|
|
7357
7352
|
}
|
|
7353
|
+
}
|
|
7358
7354
|
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
visible?: boolean
|
|
7364
|
-
}>(), {
|
|
7365
|
-
showMenu: true,
|
|
7366
|
-
toolbarMode: 'auto',
|
|
7367
|
-
visible: true
|
|
7368
|
-
})
|
|
7355
|
+
const onFileChange = async (e: Event) => {
|
|
7356
|
+
const target = e.target as HTMLInputElement
|
|
7357
|
+
const files = target.files
|
|
7358
|
+
if (!files || files.length === 0) return
|
|
7369
7359
|
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
presentationTitle: presentationTitle.value,
|
|
7379
|
-
isEditingTitle: isEditingTitle.value,
|
|
7380
|
-
startEditingTitle,
|
|
7381
|
-
stopEditingTitle,
|
|
7382
|
-
zoomRatio: zoomRatio.value,
|
|
7383
|
-
setZoomRatio: (val: number) => { zoomRatio.value = val },
|
|
7384
|
-
resetZoom,
|
|
7385
|
-
isMasterEditingView: isMasterEditingView.value,
|
|
7386
|
-
enterMasterView,
|
|
7387
|
-
exitMasterView,
|
|
7388
|
-
startPresentation,
|
|
7389
|
-
startFromBeginning,
|
|
7390
|
-
triggerPPTXImport,
|
|
7391
|
-
triggerJSONImport
|
|
7392
|
-
}
|
|
7393
|
-
for (const k of Object.keys(sp)) {
|
|
7394
|
-
if (k === 'deck' || k in merged) continue
|
|
7395
|
-
merged[k] = sp[k]
|
|
7396
|
-
}
|
|
7397
|
-
if ('deck' in sp) {
|
|
7398
|
-
Object.defineProperty(merged, 'deck', {
|
|
7399
|
-
get: () => sp.deck,
|
|
7400
|
-
enumerable: true,
|
|
7401
|
-
configurable: true
|
|
7402
|
-
})
|
|
7360
|
+
const file = files[0]
|
|
7361
|
+
const reader = new FileReader()
|
|
7362
|
+
reader.onload = async (event) => {
|
|
7363
|
+
try {
|
|
7364
|
+
const buffer = event.target?.result as ArrayBuffer
|
|
7365
|
+
await importPptxArrayBuffer(buffer, file.name)
|
|
7366
|
+
} finally {
|
|
7367
|
+
target.value = ''
|
|
7403
7368
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7369
|
+
}
|
|
7370
|
+
reader.readAsArrayBuffer(file)
|
|
7371
|
+
}
|
|
7406
7372
|
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7373
|
+
const props = withDefaults(defineProps<{
|
|
7374
|
+
slotProps?: any
|
|
7375
|
+
showMenu?: boolean
|
|
7376
|
+
toolbarMode?: 'auto' | 'more' | 'full'
|
|
7377
|
+
visible?: boolean
|
|
7378
|
+
}>(), {
|
|
7379
|
+
showMenu: true,
|
|
7380
|
+
toolbarMode: 'auto',
|
|
7381
|
+
visible: true
|
|
7382
|
+
})
|
|
7383
|
+
|
|
7384
|
+
// 不能整体展开 slotProps:其 deck 是惰性 getter,展开会立刻执行 getDeck() 整册深拷贝
|
|
7385
|
+
// (110 页实测 ~500ms/次),且展开发生在本 computed 内会把本 computed 订阅到全册,
|
|
7386
|
+
// 拖拽/打字每步都失效重算。改为逐键拷贝(跳过 deck),deck 仍以 getter 形式透传,
|
|
7387
|
+
// 只在消费者真正读取时才现算
|
|
7388
|
+
const headerContext = computed(() => {
|
|
7389
|
+
const sp: any = props.slotProps || {}
|
|
7390
|
+
const merged: any = {
|
|
7391
|
+
showMenu: props.showMenu,
|
|
7392
|
+
presentationTitle: presentationTitle.value,
|
|
7393
|
+
isEditingTitle: isEditingTitle.value,
|
|
7394
|
+
startEditingTitle,
|
|
7395
|
+
stopEditingTitle,
|
|
7396
|
+
zoomRatio: zoomRatio.value,
|
|
7397
|
+
setZoomRatio: (val: number) => { zoomRatio.value = val },
|
|
7398
|
+
resetZoom,
|
|
7399
|
+
isMasterEditingView: isMasterEditingView.value,
|
|
7400
|
+
enterMasterView,
|
|
7401
|
+
exitMasterView,
|
|
7402
|
+
startPresentation,
|
|
7403
|
+
startFromBeginning,
|
|
7404
|
+
triggerPPTXImport,
|
|
7405
|
+
triggerJSONImport
|
|
7406
|
+
}
|
|
7407
|
+
for (const k of Object.keys(sp)) {
|
|
7408
|
+
if (k === 'deck' || k in merged) continue
|
|
7409
|
+
merged[k] = sp[k]
|
|
7410
|
+
}
|
|
7411
|
+
if ('deck' in sp) {
|
|
7412
|
+
Object.defineProperty(merged, 'deck', {
|
|
7413
|
+
get: () => sp.deck,
|
|
7414
|
+
enumerable: true,
|
|
7415
|
+
configurable: true
|
|
7416
|
+
})
|
|
7417
|
+
}
|
|
7418
|
+
return merged
|
|
7419
|
+
})
|
|
7420
|
+
|
|
7421
|
+
function handleToolbarWheel(e: WheelEvent) {
|
|
7422
|
+
const el = e.currentTarget as HTMLElement
|
|
7423
|
+
if (el && el.scrollWidth > el.clientWidth) {
|
|
7424
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
7425
|
+
el.scrollLeft += e.deltaY
|
|
7413
7426
|
}
|
|
7414
7427
|
}
|
|
7428
|
+
}
|
|
7415
7429
|
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7430
|
+
defineExpose({
|
|
7431
|
+
generatePptxBlob,
|
|
7432
|
+
generateImagesZipBlob,
|
|
7433
|
+
downloadAsPptx,
|
|
7434
|
+
downloadAllSlidesAsZip,
|
|
7435
|
+
downloadCurrentSlide,
|
|
7436
|
+
triggerPPTXImport,
|
|
7437
|
+
triggerJSONImport,
|
|
7438
|
+
importPptxArrayBuffer
|
|
7439
|
+
})
|
|
7426
7440
|
</script>
|
|
7427
7441
|
|
|
7428
7442
|
<template>
|
|
@@ -7438,167 +7452,178 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7438
7452
|
<!-- 外部左侧主体插槽 (默认呈现内置菜单和标题) -->
|
|
7439
7453
|
<slot name="left" v-bind="headerContext">
|
|
7440
7454
|
<el-popover v-if="showMenu" ref="menuPopoverRef" placement="bottom-start" :width="320" trigger="click"
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
<
|
|
7446
|
-
|
|
7447
|
-
</button>
|
|
7448
|
-
</template>
|
|
7449
|
-
|
|
7450
|
-
<div class="main-menu-popover">
|
|
7451
|
-
<!-- AIPPT Banner -->
|
|
7452
|
-
<div class="aippt-banner" @click="handleMenuAction(handleAIPPT)" v-if="false">
|
|
7453
|
-
<div class="aippt-icon-container">
|
|
7454
|
-
<svg class="aippt-arrow-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7455
|
-
<path d="M4.5 3L4 16.5L8.5 12.5L13.5 20L16.5 18L11.5 10.5L17 10L4.5 3Z" fill="#ffb800" stroke="#fff"
|
|
7456
|
-
stroke-width="1.5" stroke-linejoin="round" />
|
|
7457
|
-
<path d="M19 4L19.5 2.5L21 2L19.5 1.5L19 0L18.5 1.5L17 2L18.5 2.5L19 4Z" fill="#ffb800" />
|
|
7458
|
-
<path d="M21 14L21.5 12.5L23 12L21.5 11.5L21 10L20.5 11.5L19 12L20.5 12.5L21 14Z" fill="#ff9900" />
|
|
7459
|
-
<path d="M3 21L3.5 19.5L5 19L3.5 18.5L3 17L2.5 18.5L1 19L2.5 19.5L3 21Z" fill="#ffcc00" />
|
|
7455
|
+
popper-style="padding: 0; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); border: 1px solid #e2e8f0; background: #fff; overflow: hidden;">
|
|
7456
|
+
<template #reference>
|
|
7457
|
+
<button class="btn-icon" style="padding: 4px; color: var(--text-primary); flex-shrink: 0;"
|
|
7458
|
+
data-tooltip="菜单">
|
|
7459
|
+
<svg class="icon" style="width: 18px; height: 18px" viewBox="0 0 24 24">
|
|
7460
|
+
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
|
7460
7461
|
</svg>
|
|
7461
|
-
</
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7462
|
+
</button>
|
|
7463
|
+
</template>
|
|
7464
|
+
|
|
7465
|
+
<div class="main-menu-popover">
|
|
7466
|
+
<!-- AIPPT Banner -->
|
|
7467
|
+
<div class="aippt-banner" @click="handleMenuAction(handleAIPPT)" v-if="false">
|
|
7468
|
+
<div class="aippt-icon-container">
|
|
7469
|
+
<svg class="aippt-arrow-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7470
|
+
<path d="M4.5 3L4 16.5L8.5 12.5L13.5 20L16.5 18L11.5 10.5L17 10L4.5 3Z" fill="#ffb800" stroke="#fff"
|
|
7471
|
+
stroke-width="1.5" stroke-linejoin="round" />
|
|
7472
|
+
<path d="M19 4L19.5 2.5L21 2L19.5 1.5L19 0L18.5 1.5L17 2L18.5 2.5L19 4Z" fill="#ffb800" />
|
|
7473
|
+
<path d="M21 14L21.5 12.5L23 12L21.5 11.5L21 10L20.5 11.5L19 12L20.5 12.5L21 14Z" fill="#ff9900" />
|
|
7474
|
+
<path d="M3 21L3.5 19.5L5 19L3.5 18.5L3 17L2.5 18.5L1 19L2.5 19.5L3 21Z" fill="#ffcc00" />
|
|
7475
|
+
</svg>
|
|
7476
|
+
</div>
|
|
7477
|
+
<div class="aippt-text-container">
|
|
7478
|
+
<div class="aippt-title">
|
|
7479
|
+
<span style="color: #3b82f6">AIPP</span><span style="color: #d946ef">T</span>
|
|
7480
|
+
</div>
|
|
7481
|
+
<div class="aippt-desc">输入一句话,智能生成演示文稿</div>
|
|
7465
7482
|
</div>
|
|
7466
|
-
<div class="aippt-desc">输入一句话,智能生成演示文稿</div>
|
|
7467
7483
|
</div>
|
|
7468
|
-
</div>
|
|
7469
7484
|
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7485
|
+
<div class="menu-body">
|
|
7486
|
+
<!-- Import File Section -->
|
|
7487
|
+
<div class="menu-section-title">导入文件</div>
|
|
7488
|
+
<div class="import-grid">
|
|
7489
|
+
<!-- PPTX Import -->
|
|
7490
|
+
<div class="import-card" @click="handleMenuAction(triggerPPTXImport)">
|
|
7491
|
+
<div class="import-icon-box pptx-icon">
|
|
7492
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7493
|
+
<path
|
|
7494
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7495
|
+
fill="#ea4335" />
|
|
7496
|
+
<path d="M14 2V8H20L14 2Z" fill="#c5221f" />
|
|
7497
|
+
<text x="7" y="17" fill="#fff" font-size="9" font-weight="900" font-family="Arial">P</text>
|
|
7498
|
+
</svg>
|
|
7499
|
+
</div>
|
|
7500
|
+
<div class="import-label">PPTX</div>
|
|
7501
|
+
<div class="import-subtext">(仅供测试)</div>
|
|
7484
7502
|
</div>
|
|
7485
|
-
<div class="import-label">PPTX</div>
|
|
7486
|
-
<div class="import-subtext">(仅供测试)</div>
|
|
7487
|
-
</div>
|
|
7488
7503
|
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7504
|
+
<!-- JSON Import -->
|
|
7505
|
+
<div class="import-card" @click="handleMenuAction(triggerJSONImport)">
|
|
7506
|
+
<div class="import-icon-box json-icon">
|
|
7507
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7508
|
+
<path
|
|
7509
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7510
|
+
fill="#f2994a" />
|
|
7511
|
+
<path d="M14 2V8H20L14 2Z" fill="#e07c24" />
|
|
7512
|
+
<text x="7" y="17" fill="#fff" font-size="9" font-weight="900" font-family="Arial">J</text>
|
|
7513
|
+
</svg>
|
|
7514
|
+
</div>
|
|
7515
|
+
<div class="import-label">JSON</div>
|
|
7516
|
+
<div class="import-subtext">(仅供测试)</div>
|
|
7517
|
+
</div>
|
|
7518
|
+
|
|
7519
|
+
<!-- JVS-PPT Import -->
|
|
7520
|
+
<div class="import-card" @click="handleMenuAction(handleJVSPPTImport)">
|
|
7521
|
+
<div class="import-icon-box pptist-icon">
|
|
7522
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7523
|
+
<path
|
|
7524
|
+
d="M4 4C4 2.89543 4.89543 2 6 2H14L20 8V20C20 21.1046 19.1046 22 18 22H6C4.89543 22 4 21.1046 4 20V4Z"
|
|
7525
|
+
fill="#eb5757" />
|
|
7526
|
+
<path d="M14 2V8H20L14 2Z" fill="#c03930" />
|
|
7527
|
+
<rect x="7" y="12" width="10" height="2" rx="1" fill="#fff" />
|
|
7528
|
+
<rect x="7" y="16" width="10" height="2" rx="1" fill="#fff" />
|
|
7529
|
+
</svg>
|
|
7530
|
+
</div>
|
|
7531
|
+
<div class="import-label">JVS-PPT</div>
|
|
7532
|
+
<div class="import-subtext">(专属格式)</div>
|
|
7499
7533
|
</div>
|
|
7500
|
-
<div class="import-label">JSON</div>
|
|
7501
|
-
<div class="import-subtext">(仅供测试)</div>
|
|
7502
7534
|
</div>
|
|
7503
7535
|
|
|
7504
|
-
<!--
|
|
7505
|
-
<div class="
|
|
7506
|
-
|
|
7507
|
-
|
|
7536
|
+
<!-- Divider -->
|
|
7537
|
+
<div class="menu-divider"></div>
|
|
7538
|
+
|
|
7539
|
+
<!-- Link Items List -->
|
|
7540
|
+
<div class="menu-links">
|
|
7541
|
+
<!-- Export -->
|
|
7542
|
+
<el-dropdown trigger="click" @command="handleExportCommand" :disabled="isExporting"
|
|
7543
|
+
style="width: 100%; display: block; text-align: left;">
|
|
7544
|
+
<div class="menu-link-item">
|
|
7545
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7546
|
+
<path
|
|
7547
|
+
d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"
|
|
7548
|
+
fill="currentColor" />
|
|
7549
|
+
</svg>
|
|
7550
|
+
<span class="link-text">{{ isExporting ? '正在导出...' : '导出文件' }}</span>
|
|
7551
|
+
</div>
|
|
7552
|
+
<template #dropdown>
|
|
7553
|
+
<el-dropdown-menu>
|
|
7554
|
+
<el-dropdown-item command="png">下载当前页 (PNG)</el-dropdown-item>
|
|
7555
|
+
<el-dropdown-item command="jpeg">下载当前页 (JPEG)</el-dropdown-item>
|
|
7556
|
+
<el-dropdown-item command="zip">下载全部 (ZIP)</el-dropdown-item>
|
|
7557
|
+
<el-dropdown-item command="pptx">下载为 PPTX</el-dropdown-item>
|
|
7558
|
+
<el-dropdown-item command="json" divided>导出完整 JSON 配置</el-dropdown-item>
|
|
7559
|
+
<el-dropdown-item command="json-template">导出纯版式 JSON (排除图片/清空文本)</el-dropdown-item>
|
|
7560
|
+
</el-dropdown-menu>
|
|
7561
|
+
</template>
|
|
7562
|
+
</el-dropdown>
|
|
7563
|
+
|
|
7564
|
+
<!-- Reset -->
|
|
7565
|
+
<div v-if="!isReadonly" class="menu-link-item" @click="handleMenuAction(handleResetSlides)">
|
|
7566
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7508
7567
|
<path
|
|
7509
|
-
d="
|
|
7510
|
-
fill="
|
|
7511
|
-
<path d="M14 2V8H20L14 2Z" fill="#c03930" />
|
|
7512
|
-
<rect x="7" y="12" width="10" height="2" rx="1" fill="#fff" />
|
|
7513
|
-
<rect x="7" y="16" width="10" height="2" rx="1" fill="#fff" />
|
|
7568
|
+
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
|
|
7569
|
+
fill="currentColor" />
|
|
7514
7570
|
</svg>
|
|
7571
|
+
<span class="link-text">重置幻灯片</span>
|
|
7515
7572
|
</div>
|
|
7516
|
-
<div class="import-label">JVS-PPT</div>
|
|
7517
|
-
<div class="import-subtext">(专属格式)</div>
|
|
7518
|
-
</div>
|
|
7519
|
-
</div>
|
|
7520
7573
|
|
|
7521
|
-
|
|
7522
|
-
|
|
7574
|
+
<!-- Label -->
|
|
7575
|
+
<div class="menu-link-item" @click="handleMenuAction(handleLabelSlides)">
|
|
7576
|
+
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7577
|
+
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6z" fill="currentColor" />
|
|
7578
|
+
</svg>
|
|
7579
|
+
<span class="link-text">幻灯片类型标注</span>
|
|
7580
|
+
</div>
|
|
7523
7581
|
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
<!-- Export -->
|
|
7527
|
-
<el-dropdown trigger="click" @command="handleExportCommand" :disabled="isExporting"
|
|
7528
|
-
style="width: 100%; display: block; text-align: left;">
|
|
7529
|
-
<div class="menu-link-item">
|
|
7582
|
+
<!-- Shortcuts -->
|
|
7583
|
+
<div class="menu-link-item" @click="handleMenuAction(handleShortcuts)">
|
|
7530
7584
|
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7531
7585
|
<path
|
|
7532
|
-
d="
|
|
7586
|
+
d="M4 15h2v3H4v-3zm0-4h2v3H4v-3zm0-4h2v3H4V7zm4 8h8v3H8v-3zm0-4h8v3H8v-3zm0-4h8v3H8V7zm12 8h2v3h-2v-3zm0-4h2v3h-2v-3zm0-4h2v3h-2V7z"
|
|
7533
7587
|
fill="currentColor" />
|
|
7534
7588
|
</svg>
|
|
7535
|
-
<span class="link-text"
|
|
7589
|
+
<span class="link-text">快捷操作</span>
|
|
7536
7590
|
</div>
|
|
7537
|
-
<template #dropdown>
|
|
7538
|
-
<el-dropdown-menu>
|
|
7539
|
-
<el-dropdown-item command="png">下载当前页 (PNG)</el-dropdown-item>
|
|
7540
|
-
<el-dropdown-item command="jpeg">下载当前页 (JPEG)</el-dropdown-item>
|
|
7541
|
-
<el-dropdown-item command="zip">下载全部 (ZIP)</el-dropdown-item>
|
|
7542
|
-
<el-dropdown-item command="pptx">下载为 PPTX</el-dropdown-item>
|
|
7543
|
-
<el-dropdown-item command="json" divided>导出完整 JSON 配置</el-dropdown-item>
|
|
7544
|
-
<el-dropdown-item command="json-template">导出纯版式 JSON (排除图片/清空文本)</el-dropdown-item>
|
|
7545
|
-
</el-dropdown-menu>
|
|
7546
|
-
</template>
|
|
7547
|
-
</el-dropdown>
|
|
7548
|
-
|
|
7549
|
-
<!-- Reset -->
|
|
7550
|
-
<div class="menu-link-item" @click="handleMenuAction(handleResetSlides)">
|
|
7551
|
-
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7552
|
-
<path
|
|
7553
|
-
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
|
|
7554
|
-
fill="currentColor" />
|
|
7555
|
-
</svg>
|
|
7556
|
-
<span class="link-text">重置幻灯片</span>
|
|
7557
|
-
</div>
|
|
7558
7591
|
|
|
7559
|
-
<!-- Label -->
|
|
7560
|
-
<div class="menu-link-item" @click="handleMenuAction(handleLabelSlides)">
|
|
7561
|
-
<svg class="link-icon" viewBox="0 0 24 24">
|
|
7562
|
-
<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6z" fill="currentColor" />
|
|
7563
|
-
</svg>
|
|
7564
|
-
<span class="link-text">幻灯片类型标注</span>
|
|
7565
7592
|
</div>
|
|
7566
7593
|
|
|
7567
|
-
<!--
|
|
7568
|
-
<div class="menu-
|
|
7569
|
-
|
|
7570
|
-
<path
|
|
7571
|
-
d="M4 15h2v3H4v-3zm0-4h2v3H4v-3zm0-4h2v3H4V7zm4 8h8v3H8v-3zm0-4h8v3H8v-3zm0-4h8v3H8V7zm12 8h2v3h-2v-3zm0-4h2v3h-2v-3zm0-4h2v3h-2V7z"
|
|
7572
|
-
fill="currentColor" />
|
|
7573
|
-
</svg>
|
|
7574
|
-
<span class="link-text">快捷操作</span>
|
|
7594
|
+
<!-- Note Disclaimer -->
|
|
7595
|
+
<div class="menu-disclaimer">
|
|
7596
|
+
注:本站仅作测试/演示,不提供任何形式的服务
|
|
7575
7597
|
</div>
|
|
7576
|
-
|
|
7577
|
-
</div>
|
|
7578
|
-
|
|
7579
|
-
<!-- Note Disclaimer -->
|
|
7580
|
-
<div class="menu-disclaimer">
|
|
7581
|
-
注:本站仅作测试/演示,不提供任何形式的服务
|
|
7582
7598
|
</div>
|
|
7583
7599
|
</div>
|
|
7600
|
+
</el-popover>
|
|
7601
|
+
<div class="presentation-title-container"
|
|
7602
|
+
style="display: flex; align-items: center; gap: 6px; margin-left: 8px; flex-shrink: 0; min-width: 0; max-width: 280px;">
|
|
7603
|
+
<span v-if="!isEditingTitle || isReadonly" @click="startEditingTitle" class="presentation-title-text" :style="{
|
|
7604
|
+
fontSize: '15px',
|
|
7605
|
+
fontWeight: 600,
|
|
7606
|
+
color: '#0f172a',
|
|
7607
|
+
cursor: isReadonly ? 'default' : 'pointer',
|
|
7608
|
+
transition: 'color 0.15s ease',
|
|
7609
|
+
whiteSpace: 'nowrap',
|
|
7610
|
+
overflow: 'hidden',
|
|
7611
|
+
textOverflow: 'ellipsis',
|
|
7612
|
+
display: 'inline-block',
|
|
7613
|
+
maxWidth: '240px'
|
|
7614
|
+
}" :title="presentationTitle || '未命名演示文稿'">
|
|
7615
|
+
{{ presentationTitle || '未命名演示文稿' }}
|
|
7616
|
+
</span>
|
|
7617
|
+
<input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
|
|
7618
|
+
@blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
|
|
7619
|
+
style="font-size: 15px; font-weight: 600; color: #0f172a; border: 1px solid #3b82f6; border-radius: 6px; padding: 4px 8px; outline: none; background: #fff; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); width: 200px;" />
|
|
7620
|
+
<svg v-if="!isEditingTitle && !isReadonly" @click="startEditingTitle" viewBox="0 0 24 24" fill="none"
|
|
7621
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
7622
|
+
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px; flex-shrink: 0;">
|
|
7623
|
+
<path d="M12 20h9" />
|
|
7624
|
+
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
7625
|
+
</svg>
|
|
7584
7626
|
</div>
|
|
7585
|
-
</el-popover>
|
|
7586
|
-
<div class="presentation-title-container" style="display: flex; align-items: center; gap: 6px; margin-left: 8px; flex-shrink: 0; min-width: 0; max-width: 280px;">
|
|
7587
|
-
<span v-if="!isEditingTitle" @click="startEditingTitle" class="presentation-title-text"
|
|
7588
|
-
style="font-size: 15px; font-weight: 600; color: #0f172a; cursor: pointer; transition: color 0.15s ease; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; max-width: 240px;"
|
|
7589
|
-
:title="presentationTitle || '未命名演示文稿'">
|
|
7590
|
-
{{ presentationTitle || '未命名演示文稿' }}
|
|
7591
|
-
</span>
|
|
7592
|
-
<input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
|
|
7593
|
-
@blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
|
|
7594
|
-
style="font-size: 15px; font-weight: 600; color: #0f172a; border: 1px solid #3b82f6; border-radius: 6px; padding: 4px 8px; outline: none; background: #fff; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); width: 200px;" />
|
|
7595
|
-
<svg v-if="!isEditingTitle" @click="startEditingTitle" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7596
|
-
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
7597
|
-
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px; flex-shrink: 0;">
|
|
7598
|
-
<path d="M12 20h9" />
|
|
7599
|
-
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
7600
|
-
</svg>
|
|
7601
|
-
</div>
|
|
7602
7627
|
</slot>
|
|
7603
7628
|
|
|
7604
7629
|
<!-- 外部左侧后置插槽 (例如文档状态标签、副标题) -->
|
|
@@ -7606,11 +7631,13 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7606
7631
|
</div>
|
|
7607
7632
|
|
|
7608
7633
|
<!-- Center Section: Integrated Top Toolbar -->
|
|
7609
|
-
<div class="header-center-tools" @wheel.passive="handleToolbarWheel"
|
|
7634
|
+
<div v-if="!isReadonly" class="header-center-tools" @wheel.passive="handleToolbarWheel"
|
|
7635
|
+
style="flex: 1; display: flex; align-items: center; justify-content: flex-start; overflow-x: auto; overflow-y: hidden; min-width: 0; position: relative; z-index: 10; margin: 0 8px; scrollbar-width: none; -ms-overflow-style: none;">
|
|
7610
7636
|
<!-- 外部中间工具栏插槽 (默认呈现内置 Toolbar,支持前置/后置插槽扩展或整栏替换) -->
|
|
7611
7637
|
<slot name="center" v-bind="headerContext">
|
|
7612
7638
|
<slot name="toolbar" v-bind="headerContext">
|
|
7613
|
-
<div :style="{ margin: isMasterEditingView ? '0' : '0 auto' }"
|
|
7639
|
+
<div :style="{ margin: isMasterEditingView ? '0' : '0 auto' }"
|
|
7640
|
+
style="display: flex; align-items: center; flex-shrink: 0;">
|
|
7614
7641
|
<Toolbar :toolbar-mode="toolbarMode">
|
|
7615
7642
|
<template v-if="$slots['toolbar-prefix']" #prefix>
|
|
7616
7643
|
<slot name="toolbar-prefix" v-bind="headerContext"></slot>
|
|
@@ -7668,7 +7695,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7668
7695
|
</button>
|
|
7669
7696
|
|
|
7670
7697
|
<!-- Slide Master Mode Toggle Button -->
|
|
7671
|
-
<button v-if="!isMasterEditingView" class="btn-master-toggle" @click="enterMasterView()"
|
|
7698
|
+
<button v-if="!isMasterEditingView && !isReadonly" class="btn-master-toggle" @click="enterMasterView()"
|
|
7672
7699
|
style="height: 32px; padding: 0 12px; border: none; border-radius: 6px; background: #f1f5f9; color: #334155; font-size: 13px; font-weight: 500; display: flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; flex-shrink: 0;">
|
|
7673
7700
|
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor" style="flex-shrink: 0;">
|
|
7674
7701
|
<use xlink:href="#icon-muban"></use>
|
|
@@ -7696,7 +7723,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7696
7723
|
<div class="presentation-menu-item-content">
|
|
7697
7724
|
<div class="presentation-menu-item-left">
|
|
7698
7725
|
<svg viewBox="0 0 1024 1024" width="14" height="14" fill="currentColor">
|
|
7699
|
-
<path
|
|
7726
|
+
<path
|
|
7727
|
+
d="M213.333 170.667h85.334v682.666h-85.334V170.667zm170.667 0l426.667 341.333-426.667 341.333V170.667z" />
|
|
7700
7728
|
</svg>
|
|
7701
7729
|
<span>从头开始</span>
|
|
7702
7730
|
</div>
|