jvs-ppt-ui 0.0.7 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jvs-ppt-ui",
3
3
  "private": false,
4
- "version": "0.0.7",
4
+ "version": "0.0.8",
5
5
  "type": "module",
6
6
  "main": "./dist/lib/jvs-ppt-ui.umd.js",
7
7
  "module": "./dist/lib/jvs-ppt-ui.es.js",
@@ -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
- selectedElementWidth.value = `${curW}px`
1201
- selectedElementHeight.value = `${curH}px`
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
- viewportWidth.value = viewportContainerRef.value.clientWidth
1436
- viewportHeight.value = viewportContainerRef.value.clientHeight
1450
+ const cw = viewportContainerRef.value.clientWidth
1451
+ const ch = viewportContainerRef.value.clientHeight
1437
1452
 
1438
1453
  const padding = 60
1439
- const containerWidth = viewportWidth.value - padding
1440
- const containerHeight = viewportHeight.value - padding
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
- const scaleX = containerWidth / logicalWidth.value
1443
- const scaleY = containerHeight / logicalHeight.value
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
- const scale = Math.min(scaleX, scaleY)
1446
- zoomRatio.value = Math.max(20, Math.min(200, Math.round(scale * 100)))
1461
+ viewportWidth.value = cw
1462
+ viewportHeight.value = ch
1463
+ zoomRatio.value = targetZoom
1447
1464
  centerSlideInViewport()
1448
1465
  }
1449
1466
 
@@ -4126,6 +4143,27 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
4126
4143
  const isWPercent = wStr.endsWith('%')
4127
4144
  const isHPercent = hStr.endsWith('%')
4128
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
+
4129
4167
  if (!isWPercent && !isHPercent && wStr && hStr && wStr !== 'auto' && hStr !== 'auto') {
4130
4168
  const w = parseFloat(wStr)
4131
4169
  const h = parseFloat(hStr)
@@ -4221,7 +4259,9 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
4221
4259
  }
4222
4260
  }
4223
4261
 
4224
- return { width, height }
4262
+ const dims = { width, height }
4263
+ elementDimCache.set(el, { sig: dimSig, dims })
4264
+ return dims
4225
4265
  }
4226
4266
 
4227
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
- import { mockAIPptDsl } from '../utils/dslTemplate'
17
- import ShortcutsDialog from './ShortcutsDialog.vue'
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
- importSlides(JSON.parse(JSON.stringify(mockAIPptDsl)))
77
- presentationTitle.value = 'AI-DSL 智能技术排版示例'
78
- notify('成功导入 AI-DSL 演示幻灯片')
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 = () => {
@@ -7220,7 +7226,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7220
7226
  <template>
7221
7227
  <header class="app-header">
7222
7228
  <!-- Left side: Hamburger menu + Title input -->
7223
- <div style="display: flex; align-items: center; gap: 12px">
7229
+ <div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0; min-width: 0;">
7224
7230
  <!-- 外部左侧前置插槽 (例如返回按钮、Logo) -->
7225
7231
  <slot name="left-prefix" v-bind="headerContext"></slot>
7226
7232
 
@@ -7229,7 +7235,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7229
7235
  <el-popover ref="menuPopoverRef" placement="bottom-start" :width="320" trigger="click"
7230
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;">
7231
7237
  <template #reference>
7232
- <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="菜单">
7233
7239
  <svg class="icon" style="width: 18px; height: 18px" viewBox="0 0 24 24">
7234
7240
  <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
7235
7241
  </svg>
@@ -7372,17 +7378,18 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7372
7378
  </div>
7373
7379
  </div>
7374
7380
  </el-popover>
7375
- <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;">
7376
7382
  <span v-if="!isEditingTitle" @click="startEditingTitle" class="presentation-title-text"
7377
- 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 || '未命名演示文稿'">
7378
7385
  {{ presentationTitle || '未命名演示文稿' }}
7379
7386
  </span>
7380
7387
  <input v-else ref="titleInputRef" class="presentation-title-input-edit" v-model="presentationTitle"
7381
7388
  @blur="stopEditingTitle" @keyup.enter="stopEditingTitle"
7382
- 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: 240px;" />
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;" />
7383
7390
  <svg v-if="!isEditingTitle" @click="startEditingTitle" viewBox="0 0 24 24" fill="none" stroke="currentColor"
7384
7391
  stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
7385
- 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;">
7386
7393
  <path d="M12 20h9" />
7387
7394
  <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
7388
7395
  </svg>
@@ -7394,7 +7401,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7394
7401
  </div>
7395
7402
 
7396
7403
  <!-- Center Section: Integrated Top Toolbar -->
7397
- <div class="header-center-tools" :style="{ justifyContent: isMasterEditingView ? 'flex-start' : 'center' }" style="flex: 1; display: flex; align-items: center; overflow: visible; position: relative; z-index: 10; margin: 0 12px;">
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;">
7398
7405
  <!-- 外部中间工具栏插槽 (默认呈现内置 Toolbar,支持前置/后置插槽扩展或整栏替换) -->
7399
7406
  <slot name="center" v-bind="headerContext">
7400
7407
  <slot name="toolbar" v-bind="headerContext">
@@ -7411,7 +7418,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7411
7418
  </div>
7412
7419
 
7413
7420
  <!-- Right side: Utility icons -->
7414
- <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;">
7415
7422
  <!-- 外部右侧前置插槽 -->
7416
7423
  <slot name="right-prefix" v-bind="headerContext"></slot>
7417
7424
 
@@ -7422,7 +7429,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7422
7429
  <!-- Zoom Ratio Selector Dropdown -->
7423
7430
  <el-dropdown trigger="click" @command="handleZoomCommand">
7424
7431
  <div class="zoom-select-btn"
7425
- 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;">
7426
7433
  <span>{{ zoomRatio }}%</span>
7427
7434
  <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="#64748b" stroke-width="2.5">
7428
7435
  <path d="M6 9l6 6 6-6" />
@@ -7444,7 +7451,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7444
7451
 
7445
7452
  <!-- Exit Master View Button (Placed right after Zoom Ratio Dropdown) -->
7446
7453
  <button v-if="isMasterEditingView" class="btn-exit-master" @click="exitMasterView()"
7447
- 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;"
7448
7455
  title="退出母版" data-tooltip="退出母版">
7449
7456
  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#ef4444" stroke-width="2.2"
7450
7457
  stroke-linecap="round" stroke-linejoin="round">
@@ -7455,8 +7462,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7455
7462
 
7456
7463
  <!-- Slide Master Mode Toggle Button -->
7457
7464
  <button v-if="!isMasterEditingView" class="btn-master-toggle" @click="enterMasterView()"
7458
- 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;">
7459
- <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;">
7460
7467
  <use xlink:href="#icon-muban"></use>
7461
7468
  </svg>
7462
7469
  <span>母版</span>
@@ -7465,7 +7472,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7465
7472
  <!-- Presentation Mode Split Button -->
7466
7473
  <div v-if="!isMasterEditingView" class="presentation-split-btn">
7467
7474
  <button class="presentation-main-btn" @click="startFromBeginning" title="从头开始放映 (F5)">
7468
- <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;">
7469
7476
  <use xlink:href="#icon-huandengpianfangying"></use>
7470
7477
  </svg>
7471
7478
  <span>幻灯片放映</span>
@@ -7513,7 +7520,7 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7513
7520
  </div>
7514
7521
 
7515
7522
  <!-- Shortcuts Dialog Modal -->
7516
- <ShortcutsDialog v-model:visible="shortcutsDialogVisible" />
7523
+ <ShortcutsDialog v-if="shortcutsOpened" v-model:visible="shortcutsDialogVisible" />
7517
7524
  </header>
7518
7525
 
7519
7526
  <teleport to="body">
@@ -7746,6 +7753,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7746
7753
  color: #3b82f6;
7747
7754
  transition: all 0.15s ease;
7748
7755
  box-sizing: border-box;
7756
+ white-space: nowrap;
7757
+ flex-shrink: 0;
7749
7758
  }
7750
7759
 
7751
7760
  .presentation-split-btn:hover {
@@ -7767,6 +7776,8 @@ const importPptxArrayBuffer = async (buffer: ArrayBuffer, fileName?: string): Pr
7767
7776
  cursor: pointer;
7768
7777
  transition: background-color 0.15s ease;
7769
7778
  border-radius: 5px 0 0 5px;
7779
+ white-space: nowrap;
7780
+ flex-shrink: 0;
7770
7781
  }
7771
7782
 
7772
7783
  .presentation-main-btn:hover {