jvs-ppt-ui 0.0.6 → 0.0.8
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/dist/lib/jvs-ppt-ui.es.js +135084 -112697
- package/dist/lib/jvs-ppt-ui.umd.js +101 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +36 -14
- package/src/components/CanvasArea.vue +51 -13
- package/src/components/Header.vue +38 -21
- package/src/components/PptEditor.vue +1040 -1005
- package/src/components/PresentationGroupChild.vue +1 -1
- package/src/components/PresentationMode.vue +4 -2
- package/src/components/SidebarLeft.vue +15 -8
- package/src/components/Toolbar.vue +6 -4
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +1341 -1336
- package/src/composables/canvas/useCanvasTable.ts +183 -146
- package/src/composables/sidebar/useSidebarTextFormat.ts +1 -1
- package/src/composables/useSlides.ts +239 -38
- package/src/index.ts +2 -1
- package/src/utils/canvas/elementStyles.ts +1 -1
- package/dist/lib/__vite-browser-external-CbV4tiB6.js +0 -7
- package/dist/lib/dist-DkuYVyNR.js +0 -16010
- package/dist/lib/es-CfdgxdYp.js +0 -477
- package/dist/lib/jszip.min-Kba9Mb0l.js +0 -3034
- package/dist/lib/pptxgen.es-D0HrgUe_.js +0 -2352
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import PptEditor from './components/PptEditor.vue'
|
|
4
|
-
import type { DeckData, PptEditorApi } from './types'
|
|
4
|
+
import type { DeckData, PptEditorApi, UploadResourceContext } from './types'
|
|
5
5
|
|
|
6
6
|
const editorRef = ref<PptEditorApi | null>(null)
|
|
7
7
|
|
|
@@ -12,20 +12,42 @@ function handleSave(deck: DeckData) {
|
|
|
12
12
|
function handleReady(api: PptEditorApi) {
|
|
13
13
|
console.log('[JVS-PPT 独立运行模式] 编辑器初始化完成:', api)
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 模拟资源上传函数
|
|
18
|
+
* 覆盖场景:导入 PPTX/JSON、上传图片、视频、音频、剪贴板粘贴、占位符上传、背景上传等
|
|
19
|
+
*/
|
|
20
|
+
async function handleCustomUpload(fileOrBlob: File | Blob, context: UploadResourceContext): Promise<string> {
|
|
21
|
+
console.log('[模拟自定义上传函数触发] 接收到待上传资源:', {
|
|
22
|
+
type: context.type, // 资源类型: 'image' | 'video' | 'audio'
|
|
23
|
+
source: context.source, // 触发来源: 'file-upload' | 'pptx-import' | 'json-import' | 'paste' | 'placeholder' | 'background'
|
|
24
|
+
filename: context.filename,
|
|
25
|
+
size: `${(fileOrBlob.size / 1024).toFixed(2)} KB`,
|
|
26
|
+
fileOrBlob
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// 1. 模拟网络上传请求耗时
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
31
|
+
|
|
32
|
+
// 2. 实际业务生产代码范例:
|
|
33
|
+
// const formData = new FormData()
|
|
34
|
+
// formData.append('file', fileOrBlob, context.filename || `upload_${Date.now()}`)
|
|
35
|
+
// formData.append('type', context.type)
|
|
36
|
+
// const res = await fetch('/api/oss/upload', { method: 'POST', body: formData }).then(r => r.json())
|
|
37
|
+
// return res.data.url
|
|
38
|
+
|
|
39
|
+
// 3. 模拟环境:生成可正常渲染的地址作为模拟的服务器返回 URL
|
|
40
|
+
const mockServerUrl = URL.createObjectURL(fileOrBlob)
|
|
41
|
+
console.log(`[模拟上传成功] 已返回服务器文件地址:`, mockServerUrl)
|
|
42
|
+
return mockServerUrl
|
|
43
|
+
}
|
|
15
44
|
</script>
|
|
16
45
|
|
|
17
46
|
<template>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
:enable-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
material-tree-api-url="/jvs-dam/api/dam/material/tree"
|
|
25
|
-
material-page-api-url="/jvs-dam/api/dam/material/page"
|
|
26
|
-
material-api-url="/jvs-dam/api/dam/material/tree"
|
|
27
|
-
material-save-api-url="/jvs-dam/api/dam/material/save"
|
|
28
|
-
@save="handleSave"
|
|
29
|
-
@ready="handleReady"
|
|
30
|
-
/>
|
|
47
|
+
<!-- :custom-upload="handleCustomUpload" -->
|
|
48
|
+
<PptEditor ref="editorRef" :custom-upload="handleCustomUpload" :enable-mcp="true" :enable-ai="true"
|
|
49
|
+
ai-api-url="/jvs-dam/api/ppt/ai-replace" :enable-material="true"
|
|
50
|
+
material-tree-api-url="/jvs-dam/api/dam/material/tree" material-page-api-url="/jvs-dam/api/dam/material/page"
|
|
51
|
+
material-api-url="/jvs-dam/api/dam/material/tree" material-save-api-url="/jvs-dam/api/dam/material/save"
|
|
52
|
+
@save="handleSave" @ready="handleReady" />
|
|
31
53
|
</template>
|
|
@@ -965,6 +965,18 @@ function getResizeCursor(handle: string, el: any) {
|
|
|
965
965
|
// 拖拽期间全局图元尺寸缓存池:彻底消除拖动期间的 Forced Synchronous Layout(强制回流与 DOM 读取)
|
|
966
966
|
const activeDragDimensions = new Map<string, { width: number; height: number }>()
|
|
967
967
|
|
|
968
|
+
// 渲染期图元尺寸记忆化:模板对同一元素每渲染批会调 getElementWidthHeight 2-4 次,
|
|
969
|
+
// 签名含全部输入(style 尺寸/排版项、内容长度+首尾采样、选中 RO 尺寸),命中则免重复 DOM 测量
|
|
970
|
+
let elementDimCache = new WeakMap<object, { sig: string; dims: { width: number; height: number } }>()
|
|
971
|
+
watch(activeIndex, () => {
|
|
972
|
+
elementDimCache = new WeakMap()
|
|
973
|
+
})
|
|
974
|
+
if (typeof document !== 'undefined' && (document as any).fonts?.ready) {
|
|
975
|
+
;(document as any).fonts.ready.then(() => {
|
|
976
|
+
elementDimCache = new WeakMap()
|
|
977
|
+
}).catch(() => {})
|
|
978
|
+
}
|
|
979
|
+
|
|
968
980
|
const getOverlayWidth = (el: any) => {
|
|
969
981
|
if (el.type === 'line') {
|
|
970
982
|
return el.style?.width || (el.width !== undefined ? `${el.width}px` : '1px')
|
|
@@ -1197,8 +1209,11 @@ function observeSelectedElement() {
|
|
|
1197
1209
|
const curW = curIsVertical && curEditableDom
|
|
1198
1210
|
? Math.max(curEditableDom.scrollWidth, dom.offsetWidth)
|
|
1199
1211
|
: dom.offsetWidth
|
|
1200
|
-
|
|
1201
|
-
|
|
1212
|
+
// 值相等守卫:避免 RO → 渲染 → RO 的无谓链路
|
|
1213
|
+
const wStr = `${curW}px`
|
|
1214
|
+
const hStr = `${curH}px`
|
|
1215
|
+
if (selectedElementWidth.value !== wStr) selectedElementWidth.value = wStr
|
|
1216
|
+
if (selectedElementHeight.value !== hStr) selectedElementHeight.value = hStr
|
|
1202
1217
|
})
|
|
1203
1218
|
selectedElementResizeObserver.observe(dom)
|
|
1204
1219
|
if (editableDom && isVertical) {
|
|
@@ -1432,18 +1447,20 @@ function getCubeShadingPaths(el: any) {
|
|
|
1432
1447
|
function fitToScreen() {
|
|
1433
1448
|
if (!viewportContainerRef.value) return
|
|
1434
1449
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1450
|
+
const cw = viewportContainerRef.value.clientWidth
|
|
1451
|
+
const ch = viewportContainerRef.value.clientHeight
|
|
1437
1452
|
|
|
1438
1453
|
const padding = 60
|
|
1439
|
-
const
|
|
1440
|
-
const
|
|
1454
|
+
const scale = Math.min((cw - padding) / logicalWidth.value, (ch - padding) / logicalHeight.value)
|
|
1455
|
+
const targetZoom = Math.max(20, Math.min(200, Math.round(scale * 100)))
|
|
1441
1456
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1457
|
+
// 守卫:容器尺寸与目标 zoom 都与现状一致时直接返回,
|
|
1458
|
+
// 吃掉 window resize 与 viewport RO 的双触发/RO 风暴(用户手动缩放不受影响:zoomRatio≠target 时照常重适配)
|
|
1459
|
+
if (cw === viewportWidth.value && ch === viewportHeight.value && targetZoom === zoomRatio.value) return
|
|
1444
1460
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1461
|
+
viewportWidth.value = cw
|
|
1462
|
+
viewportHeight.value = ch
|
|
1463
|
+
zoomRatio.value = targetZoom
|
|
1447
1464
|
centerSlideInViewport()
|
|
1448
1465
|
}
|
|
1449
1466
|
|
|
@@ -3604,8 +3621,6 @@ function handleBorderMouseDown(e: MouseEvent, el: any) {
|
|
|
3604
3621
|
}
|
|
3605
3622
|
|
|
3606
3623
|
function handleElementDblClick(e: MouseEvent, el: any) {
|
|
3607
|
-
// TODO(debug): 定位“双击元素却新建了文本框”的拦截层,问题定位后移除
|
|
3608
|
-
console.info('[元素dblclick调试]', el?.id, el?.type)
|
|
3609
3624
|
let target = el
|
|
3610
3625
|
if (selectedElementIds.value.length > 0 && !selectedElementIds.value.includes(el.id)) {
|
|
3611
3626
|
const activeSelectedEl = findSelectedElementUnderPoint(e.clientX, e.clientY)
|
|
@@ -4128,6 +4143,27 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4128
4143
|
const isWPercent = wStr.endsWith('%')
|
|
4129
4144
|
const isHPercent = hStr.endsWith('%')
|
|
4130
4145
|
|
|
4146
|
+
// 纯数学快路径:非文本、非选中的元素,宽高都能从 style 纯算(%×基数 / px 字面量 / 无 width 归 200),
|
|
4147
|
+
// 与下方 else 分支逐条等价,但零 DOM 读、零 offset* 强制布局
|
|
4148
|
+
if (el.type !== 'text' && el.id !== selectedElementId.value && hStr && hStr !== 'auto' && (!wStr || !isNaN(parseFloat(wStr)))) {
|
|
4149
|
+
const parentGroup = findParentGroup(el.id)
|
|
4150
|
+
const baseDim = parentGroup ? getElementPixels(parentGroup) : { width: logicalWidth.value, height: logicalHeight.value }
|
|
4151
|
+
const width = !wStr
|
|
4152
|
+
? 200
|
|
4153
|
+
: (isWPercent ? (parseFloat(wStr) / 100) * baseDim.width : (parseFloat(wStr) || 200))
|
|
4154
|
+
const height = isHPercent ? (parseFloat(hStr) / 100) * baseDim.height : (parseFloat(hStr) || 100)
|
|
4155
|
+
return { width, height }
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
// 记忆化:同渲染批内模板会重复调用本函数,签名命中直接复用上次测量结果
|
|
4159
|
+
const st = el.style || {}
|
|
4160
|
+
const selW = el.id === selectedElementId.value ? selectedElementWidth.value : ''
|
|
4161
|
+
const selH = el.id === selectedElementId.value ? selectedElementHeight.value : ''
|
|
4162
|
+
const contentSig = typeof el.content === 'string' ? `${el.content.length}|${el.content.slice(0, 12)}|${el.content.slice(-12)}` : ''
|
|
4163
|
+
const dimSig = `${el.type}|${wStr}|${hStr}|${el.width || 0}|${el.height || 0}|${contentSig}|${el.latex || ''}|${st.fontSize || ''}|${st.fontFamily || ''}|${st.fontWeight || ''}|${st.lineHeight || ''}|${st.letterSpacing || ''}|${st.writingMode || ''}|${st.whiteSpace || ''}|${st.padding || ''}|${el.autoFit?.type || ''}|${el.autoFit?.fontScale || 0}|${selW}|${selH}|${logicalWidth.value}|${logicalHeight.value}|${isDragging.value ? 1 : 0}|${isResizing.value ? 1 : 0}`
|
|
4164
|
+
const dimCached = elementDimCache.get(el)
|
|
4165
|
+
if (dimCached && dimCached.sig === dimSig) return dimCached.dims
|
|
4166
|
+
|
|
4131
4167
|
if (!isWPercent && !isHPercent && wStr && hStr && wStr !== 'auto' && hStr !== 'auto') {
|
|
4132
4168
|
const w = parseFloat(wStr)
|
|
4133
4169
|
const h = parseFloat(hStr)
|
|
@@ -4223,7 +4259,9 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4223
4259
|
}
|
|
4224
4260
|
}
|
|
4225
4261
|
|
|
4226
|
-
|
|
4262
|
+
const dims = { width, height }
|
|
4263
|
+
elementDimCache.set(el, { sig: dimSig, dims })
|
|
4264
|
+
return dims
|
|
4227
4265
|
}
|
|
4228
4266
|
|
|
4229
4267
|
// Helper to get element dimensions in logical canvas pixel space
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, nextTick } from 'vue'
|
|
2
|
+
import { ref, computed, nextTick, defineAsyncComponent, watch } from 'vue'
|
|
3
3
|
import Toolbar from './Toolbar.vue'
|
|
4
4
|
import { useSlides } from '../composables/useSlides'
|
|
5
5
|
import type JSZip from 'jszip'
|
|
@@ -13,12 +13,16 @@ import { parseColorString, extractTextAlignFromHtml, updateTextAlignInHtml, norm
|
|
|
13
13
|
|
|
14
14
|
import { SHAPE_LIST as PPTX_SHAPE_LIST, SHAPE_PATH_FORMULAS as PPTX_SHAPE_PATH_FORMULAS } from '../utils/pptxShapes'
|
|
15
15
|
import { inferElementTextType, type SlideTextType } from '../utils/textTypes'
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// 快捷键弹层异步化;mock DSL 模板(~77KB)改为触发时动态加载
|
|
17
|
+
const ShortcutsDialog = defineAsyncComponent(() => import('./ShortcutsDialog.vue'))
|
|
18
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
20
|
import { sanitizeNbspInHtml, checkIsVertical } from '../composables/useSlides'
|
|
21
21
|
|
|
22
|
+
// 快捷键弹层挂载闩锁:首次打开后保持挂载,异步 chunk 只加载一次
|
|
23
|
+
const shortcutsOpened = ref(false)
|
|
24
|
+
watch(shortcutsDialogVisible, v => { if (v) shortcutsOpened.value = true })
|
|
25
|
+
|
|
22
26
|
function handleZoomCommand(command: string | number) {
|
|
23
27
|
if (command === 'reset') {
|
|
24
28
|
resetZoom()
|
|
@@ -73,9 +77,11 @@ const handleAIPPT = () => {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
const handleLoadDsl = () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
import('../utils/dslTemplate').then(m => {
|
|
81
|
+
importSlides(JSON.parse(JSON.stringify(m.mockAIPptDsl)))
|
|
82
|
+
presentationTitle.value = 'AI-DSL 智能技术排版示例'
|
|
83
|
+
notify('成功导入 AI-DSL 演示幻灯片')
|
|
84
|
+
})
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
const handleJVSPPTImport = () => {
|
|
@@ -4188,7 +4194,10 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4188
4194
|
const borderGrad = el.border?.gradient || el.outline?.gradient
|
|
4189
4195
|
const outlineGrad = borderGrad ? normalizeGradientConfig(borderGrad) : undefined
|
|
4190
4196
|
|
|
4191
|
-
const
|
|
4197
|
+
const fillVal = el.fill?.type === 'image' ? el.fill.value : null
|
|
4198
|
+
const pattern: string | undefined = fillVal
|
|
4199
|
+
? (fillVal.blob || fillVal.base64 || fillVal.src || fillVal.url || (typeof fillVal === 'string' ? fillVal : undefined))
|
|
4200
|
+
: undefined
|
|
4192
4201
|
|
|
4193
4202
|
const fill = typeof el.fill === 'string'
|
|
4194
4203
|
? (el.fill.trim() === '' ? 'none' : el.fill)
|
|
@@ -4254,6 +4263,9 @@ const parsePPTXJSONData = (json: any, zipMeta?: any) => {
|
|
|
4254
4263
|
element.style.flipH = flipH
|
|
4255
4264
|
element.style.flipV = flipV
|
|
4256
4265
|
element.style.fill = fill
|
|
4266
|
+
if (pattern) {
|
|
4267
|
+
element.style.pattern = pattern
|
|
4268
|
+
}
|
|
4257
4269
|
element.style.borderColor = el.borderColor || (el.outline?.color || 'transparent')
|
|
4258
4270
|
element.style.borderWidth = el.borderWidth !== undefined ? `${+(el.borderWidth * ratio).toFixed(2)}px` : (el.outline?.width !== undefined ? `${el.outline.width}px` : '0px')
|
|
4259
4271
|
element.style.borderStyle = el.borderType || (el.outline?.style || 'solid')
|
|
@@ -7214,7 +7226,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7214
7226
|
<template>
|
|
7215
7227
|
<header class="app-header">
|
|
7216
7228
|
<!-- Left side: Hamburger menu + Title input -->
|
|
7217
|
-
<div style="display: flex; align-items: center; gap:
|
|
7229
|
+
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0;">
|
|
7218
7230
|
<!-- 外部左侧前置插槽 (例如返回按钮、Logo) -->
|
|
7219
7231
|
<slot name="left-prefix" v-bind="headerContext"></slot>
|
|
7220
7232
|
|
|
@@ -7223,7 +7235,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7223
7235
|
<el-popover ref="menuPopoverRef" placement="bottom-start" :width="320" trigger="click"
|
|
7224
7236
|
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;">
|
|
7225
7237
|
<template #reference>
|
|
7226
|
-
<button class="btn-icon" style="padding: 4px; color: var(--text-primary)" data-tooltip="菜单">
|
|
7238
|
+
<button class="btn-icon" style="padding: 4px; color: var(--text-primary); flex-shrink: 0;" data-tooltip="菜单">
|
|
7227
7239
|
<svg class="icon" style="width: 18px; height: 18px" viewBox="0 0 24 24">
|
|
7228
7240
|
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
|
7229
7241
|
</svg>
|
|
@@ -7366,17 +7378,18 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7366
7378
|
</div>
|
|
7367
7379
|
</div>
|
|
7368
7380
|
</el-popover>
|
|
7369
|
-
<div class="presentation-title-container" style="display: flex; align-items: center; gap: 6px; margin-left: 8px;">
|
|
7381
|
+
<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;">
|
|
7370
7382
|
<span v-if="!isEditingTitle" @click="startEditingTitle" class="presentation-title-text"
|
|
7371
|
-
style="font-size: 15px; font-weight: 600; color: #0f172a; cursor: pointer; transition: color 0.15s ease;"
|
|
7383
|
+
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;"
|
|
7384
|
+
:title="presentationTitle || '未命名演示文稿'">
|
|
7372
7385
|
{{ presentationTitle || '未命名演示文稿' }}
|
|
7373
7386
|
</span>
|
|
7374
7387
|
<input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
|
|
7375
7388
|
@blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
|
|
7376
|
-
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:
|
|
7389
|
+
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;" />
|
|
7377
7390
|
<svg v-if="!isEditingTitle" @click="startEditingTitle" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7378
7391
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
|
7379
|
-
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px;">
|
|
7392
|
+
style="width: 14px; height: 14px; color: #3b82f6; cursor: pointer; margin-left: 2px; flex-shrink: 0;">
|
|
7380
7393
|
<path d="M12 20h9" />
|
|
7381
7394
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
|
7382
7395
|
</svg>
|
|
@@ -7388,7 +7401,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7388
7401
|
</div>
|
|
7389
7402
|
|
|
7390
7403
|
<!-- Center Section: Integrated Top Toolbar -->
|
|
7391
|
-
<div class="header-center-tools" :style="{ justifyContent: isMasterEditingView ? 'flex-start' : 'center' }" style="flex: 1; display: flex; align-items: center; overflow:
|
|
7404
|
+
<div class="header-center-tools" :style="{ justifyContent: isMasterEditingView ? 'flex-start' : 'center' }" style="flex: 1; display: flex; align-items: center; overflow: hidden; min-width: 0; position: relative; z-index: 10; margin: 0 8px;">
|
|
7392
7405
|
<!-- 外部中间工具栏插槽 (默认呈现内置 Toolbar,支持前置/后置插槽扩展或整栏替换) -->
|
|
7393
7406
|
<slot name="center" v-bind="headerContext">
|
|
7394
7407
|
<slot name="toolbar" v-bind="headerContext">
|
|
@@ -7405,7 +7418,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7405
7418
|
</div>
|
|
7406
7419
|
|
|
7407
7420
|
<!-- Right side: Utility icons -->
|
|
7408
|
-
<div style="display: flex; align-items: center; gap: 10px">
|
|
7421
|
+
<div style="display: flex; align-items: center; gap: 10px; flex-shrink: 0; white-space: nowrap;">
|
|
7409
7422
|
<!-- 外部右侧前置插槽 -->
|
|
7410
7423
|
<slot name="right-prefix" v-bind="headerContext"></slot>
|
|
7411
7424
|
|
|
@@ -7416,7 +7429,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7416
7429
|
<!-- Zoom Ratio Selector Dropdown -->
|
|
7417
7430
|
<el-dropdown trigger="click" @command="handleZoomCommand">
|
|
7418
7431
|
<div class="zoom-select-btn"
|
|
7419
|
-
style="height: 32px; display: flex; align-items: center; gap: 8px; padding: 0 12px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; color: #334155; cursor: pointer; background: #f1f5f9;">
|
|
7432
|
+
style="height: 32px; display: flex; align-items: center; gap: 8px; padding: 0 12px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; color: #334155; cursor: pointer; background: #f1f5f9; white-space: nowrap; flex-shrink: 0;">
|
|
7420
7433
|
<span>{{ zoomRatio }}%</span>
|
|
7421
7434
|
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="#64748b" stroke-width="2.5">
|
|
7422
7435
|
<path d="M6 9l6 6 6-6" />
|
|
@@ -7438,7 +7451,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7438
7451
|
|
|
7439
7452
|
<!-- Exit Master View Button (Placed right after Zoom Ratio Dropdown) -->
|
|
7440
7453
|
<button v-if="isMasterEditingView" class="btn-exit-master" @click="exitMasterView()"
|
|
7441
|
-
style="height: 32px; padding: 0 12px; border: 1px solid #fee2e2; border-radius: 6px; background: #fef2f2; color: #ef4444; font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: all 0.15s ease;"
|
|
7454
|
+
style="height: 32px; padding: 0 12px; border: 1px solid #fee2e2; border-radius: 6px; background: #fef2f2; color: #ef4444; font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; flex-shrink: 0;"
|
|
7442
7455
|
title="退出母版" data-tooltip="退出母版">
|
|
7443
7456
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#ef4444" stroke-width="2.2"
|
|
7444
7457
|
stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -7449,8 +7462,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7449
7462
|
|
|
7450
7463
|
<!-- Slide Master Mode Toggle Button -->
|
|
7451
7464
|
<button v-if="!isMasterEditingView" class="btn-master-toggle" @click="enterMasterView()"
|
|
7452
|
-
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;">
|
|
7453
|
-
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor">
|
|
7465
|
+
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;">
|
|
7466
|
+
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor" style="flex-shrink: 0;">
|
|
7454
7467
|
<use xlink:href="#icon-muban"></use>
|
|
7455
7468
|
</svg>
|
|
7456
7469
|
<span>母版</span>
|
|
@@ -7459,7 +7472,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7459
7472
|
<!-- Presentation Mode Split Button -->
|
|
7460
7473
|
<div v-if="!isMasterEditingView" class="presentation-split-btn">
|
|
7461
7474
|
<button class="presentation-main-btn" @click="startFromBeginning" title="从头开始放映 (F5)">
|
|
7462
|
-
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor">
|
|
7475
|
+
<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor" style="flex-shrink: 0;">
|
|
7463
7476
|
<use xlink:href="#icon-huandengpianfangying"></use>
|
|
7464
7477
|
</svg>
|
|
7465
7478
|
<span>幻灯片放映</span>
|
|
@@ -7507,7 +7520,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7507
7520
|
</div>
|
|
7508
7521
|
|
|
7509
7522
|
<!-- Shortcuts Dialog Modal -->
|
|
7510
|
-
<ShortcutsDialog v-model:visible="shortcutsDialogVisible" />
|
|
7523
|
+
<ShortcutsDialog v-if="shortcutsOpened" v-model:visible="shortcutsDialogVisible" />
|
|
7511
7524
|
</header>
|
|
7512
7525
|
|
|
7513
7526
|
<teleport to="body">
|
|
@@ -7740,6 +7753,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7740
7753
|
color: #3b82f6;
|
|
7741
7754
|
transition: all 0.15s ease;
|
|
7742
7755
|
box-sizing: border-box;
|
|
7756
|
+
white-space: nowrap;
|
|
7757
|
+
flex-shrink: 0;
|
|
7743
7758
|
}
|
|
7744
7759
|
|
|
7745
7760
|
.presentation-split-btn:hover {
|
|
@@ -7761,6 +7776,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
|
|
|
7761
7776
|
cursor: pointer;
|
|
7762
7777
|
transition: background-color 0.15s ease;
|
|
7763
7778
|
border-radius: 5px 0 0 5px;
|
|
7779
|
+
white-space: nowrap;
|
|
7780
|
+
flex-shrink: 0;
|
|
7764
7781
|
}
|
|
7765
7782
|
|
|
7766
7783
|
.presentation-main-btn:hover {
|