jvs-ppt-ui 0.0.9 → 0.0.10
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 +3 -0
- package/dist/lib/jvs-ppt-ui.es.js +22611 -21764
- package/dist/lib/jvs-ppt-ui.umd.js +102 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +53 -53
- package/src/components/AIPptDialog.vue +2 -0
- package/src/components/CanvasArea.vue +124 -62
- package/src/components/Header.vue +364 -142
- package/src/components/PptEditor.vue +26 -15
- package/src/components/PresentationMode.vue +24 -4
- package/src/components/SidebarRight.vue +8 -1
- package/src/components/SvgChart.vue +5 -2
- package/src/components/TemplateGallery.vue +1321 -866
- package/src/components/Toolbar.vue +243 -22
- package/src/components/canvas/CanvasTransformHandles.vue +5 -1
- package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
- package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
- package/src/components/canvas/textSelectionOverlay.ts +305 -0
- package/src/composables/canvas/useCanvasAlignment.ts +286 -238
- package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
- package/src/composables/canvas/useCanvasFormatting.ts +35 -12
- package/src/composables/canvas/useCanvasTable.ts +52 -24
- package/src/composables/canvas/useCanvasText.ts +12 -0
- package/src/composables/canvas/useCanvasTransform.ts +2 -2
- package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
- package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
- package/src/composables/useSlides.ts +482 -127
- package/src/style.css +88 -0
- package/src/types/index.ts +4 -0
- package/src/utils/pptxShapes.ts +1 -1
- package/src/utils/pptxShapesLazy.ts +39 -0
- package/src/utils/svgPathParser.ts +12 -1
|
@@ -11,7 +11,7 @@ import { toPoints, normalizeSvgPath } from '../utils/svgPathParser'
|
|
|
11
11
|
import { getLineElementPath, scaleSvgPath, extractPathStartEnd, isStandardShapeType } from '../utils/line'
|
|
12
12
|
import { parseColorString, extractTextAlignFromHtml, updateTextAlignInHtml, normalizeGradientConfig, getLinearDirectionByRot, getRadialDirectionByCenter } from '../utils/canvas/elementStyles'
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { shapeFormulas, shapeList as getPptxShapeList, ensurePptxShapes } from '../utils/pptxShapesLazy'
|
|
15
15
|
import { inferElementTextType, type SlideTextType } from '../utils/textTypes'
|
|
16
16
|
// 快捷键弹层异步化;mock DSL 模板(~77KB)改为触发时动态加载
|
|
17
17
|
const ShortcutsDialog = defineAsyncComponent(() => import('./ShortcutsDialog.vue'))
|
|
@@ -117,6 +117,8 @@ const onJSONFileChange = (e: Event) => {
|
|
|
117
117
|
const reader = new FileReader()
|
|
118
118
|
reader.onload = async (event) => {
|
|
119
119
|
try {
|
|
120
|
+
// JSON 装稿可能含 PPTist 原生格式,解析前确保形状预设表就绪(通常已后台预载完成)
|
|
121
|
+
await ensurePptxShapes()
|
|
120
122
|
const data = JSON.parse(event.target?.result as string)
|
|
121
123
|
if (Array.isArray(data)) {
|
|
122
124
|
if (uploadMediaResourcesInSlides) {
|
|
@@ -1463,13 +1465,88 @@ const blobToDataUrl = (blob: Blob): Promise<string> => {
|
|
|
1463
1465
|
})
|
|
1464
1466
|
}
|
|
1465
1467
|
|
|
1466
|
-
|
|
1468
|
+
/**
|
|
1469
|
+
* 将图片 DataURL 或位图通过 Canvas 转为高兼容性的 PNG Base64
|
|
1470
|
+
* 解决 PowerPoint / WPS 对 WebP 或部分 SVG 渲染兼容性差的问题
|
|
1471
|
+
*/
|
|
1472
|
+
function convertImageToPngBase64(dataUrl: string): Promise<string> {
|
|
1473
|
+
return new Promise((resolve) => {
|
|
1474
|
+
if (typeof Image === 'undefined') {
|
|
1475
|
+
resolve(dataUrl)
|
|
1476
|
+
return
|
|
1477
|
+
}
|
|
1478
|
+
const img = new Image()
|
|
1479
|
+
img.crossOrigin = 'anonymous'
|
|
1480
|
+
img.onload = () => {
|
|
1481
|
+
try {
|
|
1482
|
+
const canvas = document.createElement('canvas')
|
|
1483
|
+
canvas.width = img.naturalWidth || img.width || 400
|
|
1484
|
+
canvas.height = img.naturalHeight || img.height || 300
|
|
1485
|
+
const ctx = canvas.getContext('2d')
|
|
1486
|
+
if (ctx) {
|
|
1487
|
+
ctx.drawImage(img, 0, 0)
|
|
1488
|
+
const pngUrl = canvas.toDataURL('image/png')
|
|
1489
|
+
if (pngUrl && pngUrl.startsWith('data:image/png')) {
|
|
1490
|
+
resolve(pngUrl)
|
|
1491
|
+
return
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
// canvas 污染或导出失败
|
|
1496
|
+
}
|
|
1497
|
+
resolve(dataUrl)
|
|
1498
|
+
}
|
|
1499
|
+
img.onerror = () => resolve(dataUrl)
|
|
1500
|
+
img.src = dataUrl
|
|
1501
|
+
})
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* 创建合法兜底占位图(避免 PPTX 缺少媒体部件关系而直接报文件损坏或无法打开)
|
|
1506
|
+
*/
|
|
1507
|
+
function createFallbackImageBase64(label = '图片资源'): string {
|
|
1508
|
+
try {
|
|
1509
|
+
if (typeof document !== 'undefined') {
|
|
1510
|
+
const canvas = document.createElement('canvas')
|
|
1511
|
+
canvas.width = 400
|
|
1512
|
+
canvas.height = 300
|
|
1513
|
+
const ctx = canvas.getContext('2d')
|
|
1514
|
+
if (ctx) {
|
|
1515
|
+
ctx.fillStyle = '#f8fafc'
|
|
1516
|
+
ctx.fillRect(0, 0, 400, 300)
|
|
1517
|
+
ctx.strokeStyle = '#cbd5e1'
|
|
1518
|
+
ctx.lineWidth = 2
|
|
1519
|
+
ctx.strokeRect(4, 4, 392, 292)
|
|
1520
|
+
ctx.fillStyle = '#94a3b8'
|
|
1521
|
+
ctx.font = '14px sans-serif'
|
|
1522
|
+
ctx.textAlign = 'center'
|
|
1523
|
+
ctx.textBaseline = 'middle'
|
|
1524
|
+
ctx.fillText(label, 200, 150)
|
|
1525
|
+
return canvas.toDataURL('image/png')
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
} catch (e) { }
|
|
1529
|
+
// 1x1 透明 PNG 兜底
|
|
1530
|
+
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* 全面解析并预载多媒体资源为 Base64 DataURL,
|
|
1535
|
+
* 避免浏览器导出 PPTX 时触发跨域 XHR 失败,导致 PPT 缺少媒体文件或被 pptxgenjs 写入错误损坏二进制包
|
|
1536
|
+
*/
|
|
1537
|
+
const resolveMediaSrc = async (input: any, mediaType: 'image' | 'video' | 'audio' | 'auto' = 'auto'): Promise<string> => {
|
|
1467
1538
|
if (!input) return ''
|
|
1468
1539
|
|
|
1469
1540
|
if (typeof Blob !== 'undefined' && input instanceof Blob) {
|
|
1470
1541
|
try {
|
|
1471
1542
|
const dataUrl = await blobToDataUrl(input)
|
|
1472
|
-
if (dataUrl && dataUrl.startsWith('data:'))
|
|
1543
|
+
if (dataUrl && dataUrl.startsWith('data:')) {
|
|
1544
|
+
if (dataUrl.startsWith('data:image/webp') || dataUrl.startsWith('data:image/svg+xml')) {
|
|
1545
|
+
const png = await convertImageToPngBase64(dataUrl)
|
|
1546
|
+
if (png) return png
|
|
1547
|
+
}
|
|
1548
|
+
return dataUrl
|
|
1549
|
+
}
|
|
1473
1550
|
} catch (e) {
|
|
1474
1551
|
console.warn('Converting Blob to DataURL failed:', e)
|
|
1475
1552
|
}
|
|
@@ -1478,39 +1555,86 @@ const resolveMediaSrc = async (input: any): Promise<string> => {
|
|
|
1478
1555
|
const src = typeof input === 'string' ? input : (input.src || input.blob || input.value || input.content || '')
|
|
1479
1556
|
if (!src || typeof src !== 'string') return ''
|
|
1480
1557
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1558
|
+
// 1. 如果是 SVG 代码字符串,转为 data:image/svg+xml;base64
|
|
1559
|
+
if (src.startsWith('<svg') || src.includes('xmlns="http://www.w3.org/2000/svg"')) {
|
|
1483
1560
|
try {
|
|
1484
|
-
const
|
|
1561
|
+
const b64 = typeof window !== 'undefined' ? window.btoa(unescape(encodeURIComponent(src))) : ''
|
|
1562
|
+
if (b64) return `data:image/svg+xml;base64,${b64}`
|
|
1563
|
+
} catch (e) { }
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// 2. 如果本身已是 DataURL
|
|
1567
|
+
if (src.startsWith('data:')) {
|
|
1568
|
+
if (src.startsWith('data:image/webp') || src.startsWith('data:image/svg+xml')) {
|
|
1569
|
+
const png = await convertImageToPngBase64(src)
|
|
1570
|
+
if (png) return png
|
|
1571
|
+
}
|
|
1572
|
+
return src
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// 补全相对路径为完整 URL
|
|
1576
|
+
let targetUrl = src
|
|
1577
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://') && !targetUrl.startsWith('blob:')) {
|
|
1578
|
+
if (typeof window !== 'undefined') {
|
|
1579
|
+
const origin = window.location.origin
|
|
1580
|
+
targetUrl = targetUrl.startsWith('/') ? `${origin}${targetUrl}` : `${origin}/${targetUrl}`
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 3. 方式 A: 优先使用 fetch 获取真实媒体二进制流(支持 blob:、相对路径、带 CORS 的 HTTP 资源)
|
|
1585
|
+
try {
|
|
1586
|
+
const res = await fetch(targetUrl, { mode: 'cors' })
|
|
1587
|
+
if (res.ok) {
|
|
1485
1588
|
const blob = await res.blob()
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1589
|
+
if (blob && blob.size > 0) {
|
|
1590
|
+
const dataUrl = await blobToDataUrl(blob)
|
|
1591
|
+
if (dataUrl && dataUrl.startsWith('data:')) {
|
|
1592
|
+
if (dataUrl.startsWith('data:image/webp') || dataUrl.startsWith('data:image/svg+xml')) {
|
|
1593
|
+
const png = await convertImageToPngBase64(dataUrl)
|
|
1594
|
+
if (png) return png
|
|
1595
|
+
}
|
|
1596
|
+
return dataUrl
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1490
1599
|
}
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
// fetch 失败(常见于跨域限制、网络异常),转入回退处理
|
|
1602
|
+
}
|
|
1491
1603
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1604
|
+
// 4. 方式 B: XMLHttpRequest 回退
|
|
1605
|
+
try {
|
|
1606
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
1607
|
+
const xhr = new XMLHttpRequest()
|
|
1608
|
+
xhr.open('GET', targetUrl, true)
|
|
1609
|
+
xhr.responseType = 'blob'
|
|
1610
|
+
xhr.onload = () => {
|
|
1611
|
+
if ((xhr.status === 200 || xhr.status === 0) && xhr.response && xhr.response.size > 0) {
|
|
1612
|
+
resolve(xhr.response)
|
|
1613
|
+
} else {
|
|
1614
|
+
reject(new Error(`XHR status ${xhr.status}`))
|
|
1501
1615
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1616
|
+
}
|
|
1617
|
+
xhr.onerror = reject
|
|
1618
|
+
xhr.ontimeout = reject
|
|
1619
|
+
xhr.timeout = 8000
|
|
1620
|
+
xhr.send()
|
|
1621
|
+
})
|
|
1622
|
+
const dataUrl = await blobToDataUrl(blob)
|
|
1623
|
+
if (dataUrl && dataUrl.startsWith('data:')) {
|
|
1624
|
+
if (dataUrl.startsWith('data:image/webp') || dataUrl.startsWith('data:image/svg+xml')) {
|
|
1625
|
+
const png = await convertImageToPngBase64(dataUrl)
|
|
1626
|
+
if (png) return png
|
|
1627
|
+
}
|
|
1628
|
+
return dataUrl
|
|
1509
1629
|
}
|
|
1630
|
+
} catch (e) {
|
|
1631
|
+
// XHR 回退失败
|
|
1632
|
+
}
|
|
1510
1633
|
|
|
1511
|
-
|
|
1634
|
+
// 5. 方式 C: 如果是图片资源,通过 Image + Canvas 获取(浏览器对 img 的跨域加载限制通常比 fetch/xhr 宽松)
|
|
1635
|
+
if (mediaType === 'image' || mediaType === 'auto') {
|
|
1512
1636
|
try {
|
|
1513
|
-
|
|
1637
|
+
const canvasData = await new Promise<string>((resolve) => {
|
|
1514
1638
|
const img = new Image()
|
|
1515
1639
|
img.crossOrigin = 'anonymous'
|
|
1516
1640
|
img.onload = () => {
|
|
@@ -1522,34 +1646,31 @@ const resolveMediaSrc = async (input: any): Promise<string> => {
|
|
|
1522
1646
|
if (ctx) {
|
|
1523
1647
|
ctx.drawImage(img, 0, 0)
|
|
1524
1648
|
const dataUrl = canvas.toDataURL('image/png')
|
|
1525
|
-
if (dataUrl && dataUrl.startsWith('data:image')) {
|
|
1649
|
+
if (dataUrl && dataUrl.startsWith('data:image/png')) {
|
|
1526
1650
|
resolve(dataUrl)
|
|
1527
1651
|
return
|
|
1528
1652
|
}
|
|
1529
1653
|
}
|
|
1530
1654
|
} catch (err) {
|
|
1531
|
-
|
|
1655
|
+
// Tainted canvas
|
|
1532
1656
|
}
|
|
1533
1657
|
resolve('')
|
|
1534
1658
|
}
|
|
1535
1659
|
img.onerror = () => resolve('')
|
|
1536
|
-
img.src =
|
|
1660
|
+
img.src = targetUrl
|
|
1537
1661
|
})
|
|
1662
|
+
if (canvasData) return canvasData
|
|
1538
1663
|
} catch (e) {
|
|
1539
|
-
|
|
1540
|
-
return ''
|
|
1664
|
+
// ignore
|
|
1541
1665
|
}
|
|
1542
1666
|
}
|
|
1543
1667
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
} else {
|
|
1548
|
-
return (typeof window !== 'undefined' ? window.location.origin : '') + '/' + src
|
|
1549
|
-
}
|
|
1668
|
+
// 6. 兜底保护:如果是图片类型,返回有效占位图,确保 pptxgenjs 能写入合法的 media 文件,绝不产生损坏的 pptx
|
|
1669
|
+
if (mediaType === 'image') {
|
|
1670
|
+
return createFallbackImageBase64('图片资源加载失败')
|
|
1550
1671
|
}
|
|
1551
1672
|
|
|
1552
|
-
return
|
|
1673
|
+
return ''
|
|
1553
1674
|
}
|
|
1554
1675
|
|
|
1555
1676
|
const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
@@ -1599,11 +1720,13 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
1599
1720
|
pptxBg = { color: parsed.color, transparency: parsed.transparency }
|
|
1600
1721
|
} else if (bgObj.type === 'image' && bgObj.image) {
|
|
1601
1722
|
const src = bgObj.image.src || bgObj.image
|
|
1602
|
-
const bgUrl = await resolveMediaSrc(src)
|
|
1603
|
-
if (bgUrl.startsWith('data:')) {
|
|
1723
|
+
const bgUrl = await resolveMediaSrc(src, 'image')
|
|
1724
|
+
if (bgUrl && bgUrl.startsWith('data:')) {
|
|
1604
1725
|
pptxBg = { data: bgUrl }
|
|
1605
|
-
} else {
|
|
1726
|
+
} else if (bgUrl) {
|
|
1606
1727
|
pptxBg = { path: bgUrl }
|
|
1728
|
+
} else {
|
|
1729
|
+
pptxBg = { color: 'FFFFFF', transparency: 0 }
|
|
1607
1730
|
}
|
|
1608
1731
|
} else if (bgObj.type === 'gradient' && bgObj.gradient) {
|
|
1609
1732
|
const colors = bgObj.gradient.colors || []
|
|
@@ -1960,7 +2083,10 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
1960
2083
|
}
|
|
1961
2084
|
}
|
|
1962
2085
|
else if (el.type === 'image') {
|
|
1963
|
-
let imgPath = await resolveMediaSrc(el.content || (el as any).src || '')
|
|
2086
|
+
let imgPath = await resolveMediaSrc(el.content || (el as any).src || '', 'image')
|
|
2087
|
+
if (!imgPath) {
|
|
2088
|
+
imgPath = createFallbackImageBase64('图片缺失')
|
|
2089
|
+
}
|
|
1964
2090
|
|
|
1965
2091
|
const imgOpts: pptxgen.ImageProps = {
|
|
1966
2092
|
x, y, w, h,
|
|
@@ -2106,7 +2232,7 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
2106
2232
|
const buildNativeShapeFill = async (): Promise<any> => {
|
|
2107
2233
|
// 图片填充 → 原生 a:blipFill
|
|
2108
2234
|
if (patternUrl) {
|
|
2109
|
-
const pUrl = await resolveMediaSrc(patternUrl)
|
|
2235
|
+
const pUrl = await resolveMediaSrc(patternUrl, 'image')
|
|
2110
2236
|
if (pUrl && /^data:image\/svg\+xml/i.test(pUrl)) {
|
|
2111
2237
|
const pngUrl = await rasterizeSvgFill(pUrl, pixelWidth, pixelHeight)
|
|
2112
2238
|
return pngUrl ? { type: 'image', data: pngUrl } : undefined
|
|
@@ -2529,9 +2655,9 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
2529
2655
|
}
|
|
2530
2656
|
}
|
|
2531
2657
|
if (!mediaPath) {
|
|
2532
|
-
mediaPath = await resolveMediaSrc(mediaInput)
|
|
2658
|
+
mediaPath = await resolveMediaSrc(mediaInput, el.type as 'video' | 'audio')
|
|
2533
2659
|
}
|
|
2534
|
-
if (mediaPath) {
|
|
2660
|
+
if (mediaPath && mediaPath.startsWith('data:')) {
|
|
2535
2661
|
let extn = el.type === 'audio' ? 'mp3' : 'mp4'
|
|
2536
2662
|
if (mediaPath.startsWith('data:audio/')) {
|
|
2537
2663
|
if (mediaPath.includes('audio/wav')) extn = 'wav'
|
|
@@ -2544,23 +2670,40 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
|
|
|
2544
2670
|
else extn = 'mp4'
|
|
2545
2671
|
}
|
|
2546
2672
|
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2673
|
+
pptxSlide.addMedia({
|
|
2674
|
+
type: el.type,
|
|
2675
|
+
data: mediaPath,
|
|
2676
|
+
extn,
|
|
2677
|
+
x, y, w, h,
|
|
2678
|
+
objectName: el.id
|
|
2679
|
+
})
|
|
2680
|
+
} else {
|
|
2681
|
+
// 无法获取或跨域拦截二进制数据时的防护兜底:
|
|
2682
|
+
// 严禁调用带 path 的 addMedia,否则 pptxgenjs 会因为 XHR 失败向媒体文件写入损坏的图片数据,导致导出的 PPTX 无法打开!
|
|
2683
|
+
// 生成规范的优雅多媒体占位卡片并挂载在线超链接,确保 PPT 100% 正常打开且可点击跳转播放
|
|
2684
|
+
const isVideo = el.type === 'video'
|
|
2685
|
+
const mediaTitle = isVideo ? '视频' : '音频'
|
|
2686
|
+
const displayUrl = typeof mediaInput === 'string' ? mediaInput : ''
|
|
2687
|
+
|
|
2688
|
+
pptxSlide.addShape('rect' as any, {
|
|
2689
|
+
x, y, w, h,
|
|
2690
|
+
fill: { color: isVideo ? '1E293B' : '0F172A' },
|
|
2691
|
+
line: { color: '3B82F6', width: 1.5, dashType: 'dash' },
|
|
2692
|
+
rectRadius: 0.08,
|
|
2693
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined,
|
|
2694
|
+
objectName: el.id
|
|
2695
|
+
})
|
|
2696
|
+
|
|
2697
|
+
pptxSlide.addText([
|
|
2698
|
+
{ text: isVideo ? '▶ 视频多媒体' : '🎵 音频多媒体', options: { bold: true, fontSize: 13, color: 'FFFFFF' } },
|
|
2699
|
+
{ text: displayUrl ? `\n(在线链接: ${displayUrl.length > 36 ? displayUrl.slice(0, 36) + '...' : displayUrl})` : '', options: { fontSize: 9, color: '94A3B8' } },
|
|
2700
|
+
{ text: '\n[点击卡片在浏览器中播放]', options: { fontSize: 9, color: '60A5FA' } }
|
|
2701
|
+
], {
|
|
2702
|
+
x, y, w, h,
|
|
2703
|
+
align: 'center',
|
|
2704
|
+
valign: 'middle',
|
|
2705
|
+
hyperlink: displayUrl ? { url: displayUrl, tooltip: `点击在线打开${mediaTitle}` } : undefined
|
|
2706
|
+
})
|
|
2564
2707
|
}
|
|
2565
2708
|
}
|
|
2566
2709
|
else if (el.type === 'table') {
|
|
@@ -3027,7 +3170,7 @@ const getMaxFontSize = (html: string, defaultFontSize: number = 18): number => {
|
|
|
3027
3170
|
const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: number = 18) => {
|
|
3028
3171
|
const tagRegex = /<(div|p|li)(?![a-z0-9])[^>]*>/gi
|
|
3029
3172
|
|
|
3030
|
-
const
|
|
3173
|
+
const explicitLineHeights: number[] = []
|
|
3031
3174
|
const margins = []
|
|
3032
3175
|
let paragraphCount = 0
|
|
3033
3176
|
|
|
@@ -3055,6 +3198,7 @@ const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: numbe
|
|
|
3055
3198
|
const marginTop = getProp('margin-top')
|
|
3056
3199
|
const marginBottom = getProp('margin-bottom')
|
|
3057
3200
|
const lineHeight = getProp('line-height')
|
|
3201
|
+
const hasExplicitLineHeight = !!lineHeight
|
|
3058
3202
|
|
|
3059
3203
|
const tagStartIndex = match.index
|
|
3060
3204
|
const tagName = match[1]
|
|
@@ -3083,7 +3227,7 @@ const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: numbe
|
|
|
3083
3227
|
if (lineHeightValue < 1.1) {
|
|
3084
3228
|
lineHeightValue = 1.2
|
|
3085
3229
|
}
|
|
3086
|
-
|
|
3230
|
+
if (hasExplicitLineHeight) explicitLineHeights.push(lineHeightValue)
|
|
3087
3231
|
|
|
3088
3232
|
const isFirstParagraph = paragraphIndex === 0
|
|
3089
3233
|
const isLastParagraph = match.index + fullTag.length >= html.lastIndexOf('</' + tagName + '>')
|
|
@@ -3094,7 +3238,10 @@ const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: numbe
|
|
|
3094
3238
|
marginTopValue = parseFloat(marginTop.replace('pt', ''))
|
|
3095
3239
|
}
|
|
3096
3240
|
else if (marginTop.indexOf('em') !== -1) {
|
|
3097
|
-
|
|
3241
|
+
// 解析器把 OOXML spcPct(千分比)输出为 `val/1000 + 'em'`(如 val=20000 → "20em"),
|
|
3242
|
+
// 其真实语义是 20%(= 0.2em)。必须按百分比换算;若按字面 em × 字号会放大 100 倍,
|
|
3243
|
+
// 段间距变成几百像素。
|
|
3244
|
+
marginTopValue = parseFloat(marginTop.replace('em', '')) / 100 * maxFontSize
|
|
3098
3245
|
}
|
|
3099
3246
|
if (marginTopValue > 0) margins.push(marginTopValue)
|
|
3100
3247
|
}
|
|
@@ -3105,7 +3252,8 @@ const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: numbe
|
|
|
3105
3252
|
marginBottomValue = parseFloat(marginBottom.replace('pt', ''))
|
|
3106
3253
|
}
|
|
3107
3254
|
else if (marginBottom.indexOf('em') !== -1) {
|
|
3108
|
-
|
|
3255
|
+
// 同 margin-top:解析器的 em 实为 OOXML spcPct 百分比(见上方注释)
|
|
3256
|
+
marginBottomValue = parseFloat(marginBottom.replace('em', '')) / 100 * maxFontSize
|
|
3109
3257
|
}
|
|
3110
3258
|
if (marginBottomValue > 0) margins.push(marginBottomValue)
|
|
3111
3259
|
}
|
|
@@ -3114,8 +3262,12 @@ const getParagraphMetrics = (html: string, ratio: number, defaultFontSize: numbe
|
|
|
3114
3262
|
}
|
|
3115
3263
|
|
|
3116
3264
|
let lineHeight = 1.2
|
|
3117
|
-
|
|
3118
|
-
|
|
3265
|
+
// OOXML 的行距(lnSpc)是段落级属性:容器行高不能取各段平均值,
|
|
3266
|
+
// 否则没有显式行距的段落(默认 100%)会被其它段落的行距(如 200%)污染,
|
|
3267
|
+
// 整块文本行距虚高、溢出文本框。容器只在"所有段落都有显式行距"时取最小值兜底,
|
|
3268
|
+
// 其余情况保持默认 1.2,显式行距由段落内联 style 自行生效。
|
|
3269
|
+
if (explicitLineHeights.length && explicitLineHeights.length === paragraphCount) {
|
|
3270
|
+
lineHeight = +Math.min(...explicitLineHeights).toFixed(2)
|
|
3119
3271
|
}
|
|
3120
3272
|
|
|
3121
3273
|
let margin = 0
|
|
@@ -3664,7 +3816,7 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
3664
3816
|
const defaultThemeColors = [globalThemeColor.value || '#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4', '#70ad47']
|
|
3665
3817
|
|
|
3666
3818
|
const shapeList: any[] = []
|
|
3667
|
-
for (const item of
|
|
3819
|
+
for (const item of getPptxShapeList()) {
|
|
3668
3820
|
shapeList.push(...item.children)
|
|
3669
3821
|
}
|
|
3670
3822
|
|
|
@@ -3849,7 +4001,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
3849
4001
|
shapeEl.text!.inset = [0, 0, 0, 0]
|
|
3850
4002
|
shapeEl.style.padding = '0px'
|
|
3851
4003
|
}
|
|
3852
|
-
if (metrics.lineHeight)
|
|
4004
|
+
if (metrics.lineHeight) {
|
|
4005
|
+
shapeEl.text!.lineHeight = metrics.lineHeight
|
|
4006
|
+
shapeEl.style.lineHeight = metrics.lineHeight
|
|
4007
|
+
}
|
|
3853
4008
|
if (metrics.margin) shapeEl.text!.paragraphSpace = metrics.margin
|
|
3854
4009
|
slide.elements.push(shapeEl)
|
|
3855
4010
|
}
|
|
@@ -3983,7 +4138,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
3983
4138
|
textEl.inset = [0, 0, 0, 0]
|
|
3984
4139
|
textEl.style.padding = '0px'
|
|
3985
4140
|
}
|
|
3986
|
-
if (metrics.lineHeight)
|
|
4141
|
+
if (metrics.lineHeight) {
|
|
4142
|
+
textEl.lineHeight = metrics.lineHeight
|
|
4143
|
+
textEl.style.lineHeight = metrics.lineHeight
|
|
4144
|
+
}
|
|
3987
4145
|
if (metrics.margin) textEl.paragraphSpace = metrics.margin
|
|
3988
4146
|
slide.elements.push(textEl)
|
|
3989
4147
|
}
|
|
@@ -4285,7 +4443,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4285
4443
|
element.color = shapeTextColor
|
|
4286
4444
|
}
|
|
4287
4445
|
if (el.link) element.link = { type: 'web', target: el.link }
|
|
4288
|
-
if (metrics.lineHeight)
|
|
4446
|
+
if (metrics.lineHeight) {
|
|
4447
|
+
element.text!.lineHeight = metrics.lineHeight
|
|
4448
|
+
element.style.lineHeight = metrics.lineHeight
|
|
4449
|
+
}
|
|
4289
4450
|
if (metrics.margin) element.text!.paragraphSpace = metrics.margin
|
|
4290
4451
|
if (el.textInset) {
|
|
4291
4452
|
element.text!.inset = [
|
|
@@ -4358,7 +4519,7 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4358
4519
|
element.pathFormula = shape.pathFormula
|
|
4359
4520
|
element.viewBox = [el.width, el.height]
|
|
4360
4521
|
|
|
4361
|
-
const pathFormula =
|
|
4522
|
+
const pathFormula = shapeFormulas()[shape.pathFormula]
|
|
4362
4523
|
if (pathFormula && 'editable' in pathFormula && pathFormula.editable) {
|
|
4363
4524
|
let keypointValues = pathFormula.defaultValue
|
|
4364
4525
|
if (el.keypoints) {
|
|
@@ -4632,7 +4793,7 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4632
4793
|
}
|
|
4633
4794
|
}
|
|
4634
4795
|
}
|
|
4635
|
-
else if (el.path && el.path.indexOf('NaN') === -1) {
|
|
4796
|
+
else if (el.path && el.path.indexOf('NaN') === -1 && el.shapType !== 'custom') {
|
|
4636
4797
|
if (el.shapType === 'arc') {
|
|
4637
4798
|
element.path = el.path
|
|
4638
4799
|
element.viewBox = [originWidth, originHeight]
|
|
@@ -4656,22 +4817,14 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4656
4817
|
}
|
|
4657
4818
|
}
|
|
4658
4819
|
if (el.shapType === 'custom') {
|
|
4659
|
-
let pathInput = el.path
|
|
4820
|
+
let pathInput = el.path || ''
|
|
4660
4821
|
if (pathInput.indexOf('NaN') !== -1) {
|
|
4661
4822
|
if (element.width === 0) element.width = 0.1
|
|
4662
4823
|
if (element.height === 0) element.height = 0.1
|
|
4663
4824
|
pathInput = pathInput.replace(/NaN/g, '0')
|
|
4664
4825
|
}
|
|
4665
|
-
|
|
4666
|
-
element.
|
|
4667
|
-
const w = normalized.width
|
|
4668
|
-
const h = normalized.height
|
|
4669
|
-
if ((w / h) > (originWidth / originHeight)) {
|
|
4670
|
-
element.viewBox = [w, w * originHeight / originWidth]
|
|
4671
|
-
}
|
|
4672
|
-
else {
|
|
4673
|
-
element.viewBox = [h * originWidth / originHeight, h]
|
|
4674
|
-
}
|
|
4826
|
+
element.path = pathInput
|
|
4827
|
+
element.viewBox = [originWidth, originHeight]
|
|
4675
4828
|
}
|
|
4676
4829
|
|
|
4677
4830
|
if (element.path && typeof element.path === 'string') {
|
|
@@ -4906,7 +5059,7 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4906
5059
|
|
|
4907
5060
|
const normType = String(shapType).toLowerCase()
|
|
4908
5061
|
|
|
4909
|
-
for (const group of
|
|
5062
|
+
for (const group of getPptxShapeList()) {
|
|
4910
5063
|
for (const item of group.children) {
|
|
4911
5064
|
const itemPptxType = (item.pptxShapeType || '').toLowerCase()
|
|
4912
5065
|
const itemFormula = (item.pathFormula || '').toLowerCase()
|
|
@@ -4927,8 +5080,8 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4927
5080
|
const matchedShapType = item.pptxShapeType || shapType
|
|
4928
5081
|
const matchedPathFormula = item.pathFormula
|
|
4929
5082
|
|
|
4930
|
-
if (item.pathFormula &&
|
|
4931
|
-
const formula =
|
|
5083
|
+
if (item.pathFormula && shapeFormulas()[item.pathFormula]) {
|
|
5084
|
+
const formula = shapeFormulas()[item.pathFormula]
|
|
4932
5085
|
let kps = formula.defaultValue ? [...formula.defaultValue] : []
|
|
4933
5086
|
if (_element.keypoints) {
|
|
4934
5087
|
if (normType === 'roundrect' || normType === 'snip1rect' || normType === 'cutrectsingle' || normType === 'round1rect' || normType === 'roundrectsingle') {
|
|
@@ -5094,18 +5247,27 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
5094
5247
|
const isChildVertical = checkIsVertical(_el)
|
|
5095
5248
|
|
|
5096
5249
|
// 提取原生字号,防止 CanvasShapeElement 回退到 16px 导致小形状文本溢出/折行被裁切
|
|
5250
|
+
// 必须取所有 run 字号的最小值:容器字号决定 <p> 的 strut(字号 × lineHeight),
|
|
5251
|
+
// 若取首个字号(如标题 21px)会把 16px 正文行撑到 27px+,行距虚高、文字溢出文本框;
|
|
5252
|
+
// 各 run 自身字号仍由内联 span 生效,取最小值不影响大字号标题的渲染。
|
|
5097
5253
|
let childFontSize = _el.fontSize || _el.style?.fontSize
|
|
5098
5254
|
if (!childFontSize && (_el.content || _el.text)) {
|
|
5099
5255
|
const textToProbe = typeof _el.content === 'string' ? _el.content : (typeof _el.text === 'string' ? _el.text : (_el.text?.content || ''))
|
|
5100
|
-
const
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
const pxMatch = textToProbe.match(/font-size\s*:\s*([\d.]+)px/i)
|
|
5105
|
-
if (pxMatch) {
|
|
5106
|
-
childFontSize = Math.round(parseFloat(pxMatch[1]))
|
|
5107
|
-
}
|
|
5256
|
+
const sizeCandidates: number[] = []
|
|
5257
|
+
for (const m of textToProbe.matchAll(/font-size\s*:\s*([\d.]+)\s*(pt|px)/gi)) {
|
|
5258
|
+
const v = parseFloat(m[1])
|
|
5259
|
+
sizeCandidates.push(m[2].toLowerCase() === 'pt' ? Math.round(v * ratio) : Math.round(v))
|
|
5108
5260
|
}
|
|
5261
|
+
if (sizeCandidates.length) childFontSize = Math.min(...sizeCandidates)
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
// 组内文本与顶层文本走同一套段落度量(getParagraphMetrics):
|
|
5265
|
+
// 容器 lineHeight(默认 1.2)与段间距,否则 contenteditable 回退 'normal'(≈1.3~1.43)
|
|
5266
|
+
// 同样会造成行距虚高,与容器字号问题叠加后正文明显比原 PPT 疏。
|
|
5267
|
+
let childMetrics: { lineHeight: number; margin: number | null } | null = null
|
|
5268
|
+
const childHtmlForMetrics = typeof _el.content === 'string' ? _el.content : (typeof _el.text === 'string' ? _el.text : (_el.text?.content || ''))
|
|
5269
|
+
if (/<(p|div|li)[\s>]/i.test(childHtmlForMetrics)) {
|
|
5270
|
+
childMetrics = getParagraphMetrics(childHtmlForMetrics, ratio, childFontSize || 16)
|
|
5109
5271
|
}
|
|
5110
5272
|
|
|
5111
5273
|
const rawChildLineCap = _el.borderLineCap || _el.border?.lineCap || _el.lineCap
|
|
@@ -5130,6 +5292,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
5130
5292
|
fontSize: childFontSize ? `${childFontSize}px` : undefined,
|
|
5131
5293
|
whiteSpace: (_el.wrap === 'none' || _el.wrap === false) ? 'nowrap' : (_el.style?.whiteSpace || 'normal'),
|
|
5132
5294
|
}
|
|
5295
|
+
// 段落度量落到容器样式:lineHeight 撑住 strut、paragraphSpace 驱动段间距(px 数值,
|
|
5296
|
+
// CanvasTextElement 以 element.style.paragraphSpace 组装 --paragraphSpace)
|
|
5297
|
+
if (childMetrics?.lineHeight) childStyle.lineHeight = childMetrics.lineHeight
|
|
5298
|
+
if (childMetrics?.margin) childStyle.paragraphSpace = childMetrics.margin
|
|
5133
5299
|
if (_el.reflection) {
|
|
5134
5300
|
childStyle.reflection = {
|
|
5135
5301
|
enabled: _el.reflection.enabled,
|
|
@@ -6577,6 +6743,7 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
6577
6743
|
registerPPTParser(parsePPTXJSONData)
|
|
6578
6744
|
|
|
6579
6745
|
const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Promise<boolean> => {
|
|
6746
|
+
await ensurePptxShapes() // PPTX 解析依赖形状预设表(已后台预载,此处通常瞬时)
|
|
6580
6747
|
const nameWithoutExt = fileName ? (fileName.substring(0, fileName.lastIndexOf('.')) || fileName) : presentationTitle.value || '演示文稿'
|
|
6581
6748
|
|
|
6582
6749
|
loadingText.value = '正在导入 PPTX,请稍候...'
|
|
@@ -7192,28 +7359,59 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7192
7359
|
const props = withDefaults(defineProps<{
|
|
7193
7360
|
slotProps?: any
|
|
7194
7361
|
showMenu?: boolean
|
|
7362
|
+
toolbarMode?: 'auto' | 'more' | 'full'
|
|
7363
|
+
visible?: boolean
|
|
7195
7364
|
}>(), {
|
|
7196
|
-
showMenu: true
|
|
7365
|
+
showMenu: true,
|
|
7366
|
+
toolbarMode: 'auto',
|
|
7367
|
+
visible: true
|
|
7197
7368
|
})
|
|
7198
7369
|
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7370
|
+
// 不能整体展开 slotProps:其 deck 是惰性 getter,展开会立刻执行 getDeck() 整册深拷贝
|
|
7371
|
+
// (110 页实测 ~500ms/次),且展开发生在本 computed 内会把本 computed 订阅到全册,
|
|
7372
|
+
// 拖拽/打字每步都失效重算。改为逐键拷贝(跳过 deck),deck 仍以 getter 形式透传,
|
|
7373
|
+
// 只在消费者真正读取时才现算
|
|
7374
|
+
const headerContext = computed(() => {
|
|
7375
|
+
const sp: any = props.slotProps || {}
|
|
7376
|
+
const merged: any = {
|
|
7377
|
+
showMenu: props.showMenu,
|
|
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
|
+
})
|
|
7403
|
+
}
|
|
7404
|
+
return merged
|
|
7405
|
+
})
|
|
7406
|
+
|
|
7407
|
+
function handleToolbarWheel(e: WheelEvent) {
|
|
7408
|
+
const el = e.currentTarget as HTMLElement
|
|
7409
|
+
if (el && el.scrollWidth > el.clientWidth) {
|
|
7410
|
+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
|
7411
|
+
el.scrollLeft += e.deltaY
|
|
7412
|
+
}
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7217
7415
|
|
|
7218
7416
|
defineExpose({
|
|
7219
7417
|
generatePptxBlob,
|
|
@@ -7228,7 +7426,10 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7228
7426
|
</script>
|
|
7229
7427
|
|
|
7230
7428
|
<template>
|
|
7231
|
-
|
|
7429
|
+
<!-- v-show 落在内层根元素而非组件本身:组件是多根节点(含 teleport),
|
|
7430
|
+
组件级运行时指令会触发 Vue "non-element root node" 警告,
|
|
7431
|
+
该警告在 dev 下会被 Vite 的 console 转发炸成异常并中断渲染 -->
|
|
7432
|
+
<header v-show="visible" class="app-header">
|
|
7232
7433
|
<!-- Left side: Hamburger menu + Title input -->
|
|
7233
7434
|
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0;">
|
|
7234
7435
|
<!-- 外部左侧前置插槽 (例如返回按钮、Logo) -->
|
|
@@ -7405,18 +7606,20 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7405
7606
|
</div>
|
|
7406
7607
|
|
|
7407
7608
|
<!-- Center Section: Integrated Top Toolbar -->
|
|
7408
|
-
<div class="header-center-tools"
|
|
7609
|
+
<div class="header-center-tools" @wheel.passive="handleToolbarWheel" 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;">
|
|
7409
7610
|
<!-- 外部中间工具栏插槽 (默认呈现内置 Toolbar,支持前置/后置插槽扩展或整栏替换) -->
|
|
7410
7611
|
<slot name="center" v-bind="headerContext">
|
|
7411
7612
|
<slot name="toolbar" v-bind="headerContext">
|
|
7412
|
-
<
|
|
7413
|
-
<
|
|
7414
|
-
<
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
<
|
|
7418
|
-
|
|
7419
|
-
|
|
7613
|
+
<div :style="{ margin: isMasterEditingView ? '0' : '0 auto' }" style="display: flex; align-items: center; flex-shrink: 0;">
|
|
7614
|
+
<Toolbar :toolbar-mode="toolbarMode">
|
|
7615
|
+
<template v-if="$slots['toolbar-prefix']" #prefix>
|
|
7616
|
+
<slot name="toolbar-prefix" v-bind="headerContext"></slot>
|
|
7617
|
+
</template>
|
|
7618
|
+
<template v-if="$slots['toolbar-suffix']" #suffix>
|
|
7619
|
+
<slot name="toolbar-suffix" v-bind="headerContext"></slot>
|
|
7620
|
+
</template>
|
|
7621
|
+
</Toolbar>
|
|
7622
|
+
</div>
|
|
7420
7623
|
</slot>
|
|
7421
7624
|
</slot>
|
|
7422
7625
|
</div>
|
|
@@ -7748,7 +7951,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7748
7951
|
}
|
|
7749
7952
|
|
|
7750
7953
|
.presentation-split-btn {
|
|
7751
|
-
display: flex;
|
|
7954
|
+
display: inline-flex !important;
|
|
7752
7955
|
align-items: center;
|
|
7753
7956
|
height: 32px;
|
|
7754
7957
|
border: 1px solid #3b82f6;
|
|
@@ -7756,9 +7959,11 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7756
7959
|
background: #ffffff;
|
|
7757
7960
|
color: #3b82f6;
|
|
7758
7961
|
transition: all 0.15s ease;
|
|
7759
|
-
box-sizing: border-box;
|
|
7760
|
-
white-space: nowrap;
|
|
7761
|
-
flex-shrink: 0;
|
|
7962
|
+
box-sizing: border-box !important;
|
|
7963
|
+
white-space: nowrap !important;
|
|
7964
|
+
flex-shrink: 0 !important;
|
|
7965
|
+
position: relative;
|
|
7966
|
+
overflow: hidden;
|
|
7762
7967
|
}
|
|
7763
7968
|
|
|
7764
7969
|
.presentation-split-btn:hover {
|
|
@@ -7767,25 +7972,42 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7767
7972
|
}
|
|
7768
7973
|
|
|
7769
7974
|
.presentation-main-btn {
|
|
7770
|
-
display: flex;
|
|
7975
|
+
display: inline-flex !important;
|
|
7771
7976
|
align-items: center;
|
|
7772
7977
|
gap: 6px;
|
|
7773
|
-
height: 100
|
|
7774
|
-
padding: 0 10px 0 12px;
|
|
7775
|
-
background: transparent;
|
|
7776
|
-
border: none;
|
|
7777
|
-
color: #3b82f6;
|
|
7778
|
-
font-size: 13px;
|
|
7779
|
-
font-weight: 600;
|
|
7978
|
+
height: 100% !important;
|
|
7979
|
+
padding: 0 10px 0 12px !important;
|
|
7980
|
+
background: transparent !important;
|
|
7981
|
+
border: none !important;
|
|
7982
|
+
color: #3b82f6 !important;
|
|
7983
|
+
font-size: 13px !important;
|
|
7984
|
+
font-weight: 600 !important;
|
|
7780
7985
|
cursor: pointer;
|
|
7781
7986
|
transition: background-color 0.15s ease;
|
|
7782
7987
|
border-radius: 5px 0 0 5px;
|
|
7783
|
-
white-space: nowrap;
|
|
7784
|
-
flex-shrink: 0;
|
|
7988
|
+
white-space: nowrap !important;
|
|
7989
|
+
flex-shrink: 0 !important;
|
|
7990
|
+
line-height: 1 !important;
|
|
7991
|
+
box-sizing: border-box !important;
|
|
7992
|
+
position: relative;
|
|
7993
|
+
}
|
|
7994
|
+
|
|
7995
|
+
.presentation-main-btn::before,
|
|
7996
|
+
.presentation-main-btn::after {
|
|
7997
|
+
display: none !important;
|
|
7998
|
+
content: none !important;
|
|
7999
|
+
}
|
|
8000
|
+
|
|
8001
|
+
.presentation-main-btn span {
|
|
8002
|
+
white-space: nowrap !important;
|
|
8003
|
+
display: inline-block !important;
|
|
8004
|
+
line-height: 1 !important;
|
|
8005
|
+
font-size: 13px !important;
|
|
8006
|
+
color: #3b82f6 !important;
|
|
7785
8007
|
}
|
|
7786
8008
|
|
|
7787
8009
|
.presentation-main-btn:hover {
|
|
7788
|
-
background: rgba(59, 130, 246, 0.08);
|
|
8010
|
+
background: rgba(59, 130, 246, 0.08) !important;
|
|
7789
8011
|
}
|
|
7790
8012
|
|
|
7791
8013
|
.presentation-arrow-btn {
|