jvs-ppt-ui 0.0.9 → 0.0.11

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.
Files changed (45) hide show
  1. package/README.md +3 -0
  2. package/dist/lib/jvs-ppt-ui.es.js +22611 -21764
  3. package/dist/lib/jvs-ppt-ui.umd.js +102 -101
  4. package/dist/lib/style.css +1 -1
  5. package/package.json +7 -7
  6. package/scripts/check_connectors.cjs +25 -0
  7. package/scripts/check_slide1.cjs +11 -0
  8. package/scripts/check_slide123.cjs +25 -0
  9. package/scripts/convert-fonts-to-woff2.cjs +97 -0
  10. package/scripts/extract-slides.ts +149 -0
  11. package/scripts/find_candidate_pptx.cjs +34 -0
  12. package/scripts/find_orange_slide.cjs +29 -0
  13. package/scripts/generate-complete-ppt-preview.js +385 -0
  14. package/scripts/inspect_315_327.cjs +19 -0
  15. package/scripts/mcp-smoke-test.js +568 -0
  16. package/scripts/patch-pptxtojson.cjs +81 -0
  17. package/scripts/verify-fonts.cjs +20 -0
  18. package/src/App.vue +53 -53
  19. package/src/components/AIPptDialog.vue +2 -0
  20. package/src/components/CanvasArea.vue +124 -62
  21. package/src/components/Header.vue +364 -142
  22. package/src/components/PptEditor.vue +26 -15
  23. package/src/components/PresentationMode.vue +24 -4
  24. package/src/components/SidebarRight.vue +8 -1
  25. package/src/components/SvgChart.vue +5 -2
  26. package/src/components/TemplateGallery.vue +1321 -866
  27. package/src/components/Toolbar.vue +243 -22
  28. package/src/components/canvas/CanvasTransformHandles.vue +5 -1
  29. package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
  30. package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
  31. package/src/components/canvas/textSelectionOverlay.ts +305 -0
  32. package/src/composables/canvas/useCanvasAlignment.ts +286 -238
  33. package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
  34. package/src/composables/canvas/useCanvasFormatting.ts +35 -12
  35. package/src/composables/canvas/useCanvasTable.ts +52 -24
  36. package/src/composables/canvas/useCanvasText.ts +12 -0
  37. package/src/composables/canvas/useCanvasTransform.ts +2 -2
  38. package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
  39. package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
  40. package/src/composables/useSlides.ts +482 -127
  41. package/src/style.css +88 -0
  42. package/src/types/index.ts +4 -0
  43. package/src/utils/pptxShapes.ts +1 -1
  44. package/src/utils/pptxShapesLazy.ts +39 -0
  45. package/src/utils/svgPathParser.ts +12 -1
@@ -2,7 +2,7 @@ import { ref, computed, watch, nextTick, type WatchStopHandle } from 'vue'
2
2
  import { extractAnimationsFromPptxArrayBuffer } from '../utils/pptxAnimationExtractor'
3
3
  import { syncSlideConnections, scaleSvgPath, extractPathStartEnd, isEllipseShape, isStandardShapeType } from '../utils/line'
4
4
  import { getSvgPathRange } from '../utils/svgPathParser'
5
- import { SHAPE_LIST as PPTX_SHAPE_LIST, SHAPE_PATH_FORMULAS as PPTX_SHAPE_PATH_FORMULAS } from '../utils/pptxShapes'
5
+ import { shapeFormulas, shapeList, ensurePptxShapes } from '../utils/pptxShapesLazy'
6
6
  import { compileSchemaToSlides, compileSingleSchema } from '../utils/dslCompiler'
7
7
  import { defaultStyleRecipe } from './presetThemes'
8
8
  import { extractTextAlignFromHtml } from '../utils/canvas/elementStyles'
@@ -71,6 +71,8 @@ export interface SlideElement {
71
71
  keypoints?: number[]
72
72
  start?: [number, number]
73
73
  end?: [number, number]
74
+ startPoint?: [number, number]
75
+ endPoint?: [number, number]
74
76
  lineStyle?: 'solid' | 'dashed'
75
77
  points?: [string, string]
76
78
  borderHeadEnd?: { type?: string; w?: string; len?: string } | null
@@ -102,10 +104,10 @@ export interface SlideElement {
102
104
  gridColor?: string
103
105
  color?: string
104
106
  fontFamily?: string
105
- top?: string
106
- left?: string
107
- width?: string
108
- height?: string
107
+ top?: string | number
108
+ left?: string | number
109
+ width?: string | number
110
+ height?: string | number
109
111
  rotate?: number
110
112
  flipH?: boolean
111
113
  isFlipH?: boolean
@@ -2275,21 +2277,27 @@ const activeSlide = computed<Slide>({
2275
2277
  })
2276
2278
 
2277
2279
  let isSyncingConnections = false
2278
- watch(() => activeSlide.value?.elements, (newElements) => {
2279
- if (!newElements || !newElements.length) return
2280
+ // 连接线端点吸附(线跟随形状)是确定性重推导且幂等(<0.5px 不更新)。
2281
+ // 快照/深 watcher 共用同一收敛入口,避免各自为政产生不同步的几何状态。
2282
+ function settleActiveSlideConnections() {
2283
+ const cur = activeSlide.value
2284
+ if (!cur || !Array.isArray(cur.elements) || !cur.elements.length) return
2280
2285
  if (isSyncingConnections) return
2281
2286
  if (isCanvasDragging.value) return
2282
2287
 
2283
2288
  // 性能守卫:如果页面中不存在任何线条或连接线,直接跳过全量连接线更新计算
2284
- const hasLineElements = newElements.some(el => el.type === 'line')
2285
- if (!hasLineElements) return
2289
+ if (!cur.elements.some(el => el?.type === 'line')) return
2286
2290
 
2287
2291
  isSyncingConnections = true
2288
2292
  try {
2289
- syncSlideConnections(activeSlide.value, logicalWidth.value, logicalHeight.value, selectedElementIds.value)
2293
+ syncSlideConnections(cur, logicalWidth.value, logicalHeight.value, selectedElementIds.value)
2290
2294
  } finally {
2291
2295
  isSyncingConnections = false
2292
2296
  }
2297
+ }
2298
+ watch(() => activeSlide.value?.elements, (newElements) => {
2299
+ if (!newElements || !newElements.length) return
2300
+ settleActiveSlideConnections()
2293
2301
  }, { deep: true, immediate: true, flush: 'post' })
2294
2302
  const activeTab = ref<'design' | 'transition' | 'animation' | 'format' | 'multi-style' | 'multi-position' | 'material' | null>(null)
2295
2303
 
@@ -2606,6 +2614,7 @@ function revokeSlideBlobUrls(slideList: any[]) {
2606
2614
  function initHistory() {
2607
2615
  ensurePageWatchers()
2608
2616
  ensureMasterWatchers()
2617
+ slidesRevision.value++ // 导入/重置整册替换数组,补一次 bump 供对外同步
2609
2618
  // 双栈即将整体重置,此时清资源池是安全的(不会有存活条目引用池内 id)
2610
2619
  assetPool.clear()
2611
2620
  assetIntern.clear()
@@ -2673,6 +2682,11 @@ function syncDOMWithRestoredSlides() {
2673
2682
  function addHistorySnapshot() {
2674
2683
  if (isUndoingRedoing.value || pauseHistory.value) return
2675
2684
  syncActiveEditingTextToElement()
2685
+ // 连接线吸附是 flush:'post' 的深度 watcher,在手动快照之后才执行:若快照先落盘,
2686
+ // 吸附结果会被防抖 watcher 再推一条幽灵条目(同一操作两条历史,撤销第一步看似无效;
2687
+ // 还原旧条目还会让吸附反复重推导,undo 前的 flush 不断推新条目,撤销指针原地打转)。
2688
+ // 序列化前先就地收敛一次(拖拽 stopDrag 里显式调 syncSlideConnections 同理),幂等无副作用。
2689
+ settleActiveSlideConnections()
2676
2690
  ensurePageWatchers()
2677
2691
 
2678
2692
  const __perfT0 = performance.now()
@@ -2723,6 +2737,9 @@ function addHistorySnapshot() {
2723
2737
 
2724
2738
  historyIndex.value = historyStack.value.length - 1
2725
2739
  currentPageIds = slides.value.map(s => s && s.id)
2740
+ // 真实落盘的编辑统一失效缩略图/画布快照:非拖拽类编辑(换形状/样式/文字提交/对齐/
2741
+ // keypoint 调整等)原先没有任何 bump 点,SidebarLeft 激活页缩略图的 v-memo 会停在旧内容
2742
+ activeSlideVersion.value++
2726
2743
  if (import.meta.env.DEV) {
2727
2744
  console.debug(`[perf:addSnapshot] ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length} dirty=${candidates.size} stack=${historyStack.value.length}`)
2728
2745
  // 漏标脏页保险丝:未纳入候选集的页应与落盘字符串完全一致,不一致说明脏标漏页(仅 DEV)
@@ -2802,6 +2819,10 @@ function scheduleHistorySnapshot() {
2802
2819
 
2803
2820
  // 每页一个 deep watcher:任何页的任何深层变更只标脏自己那一页(替代原整册 deep watch)。
2804
2821
  // 标脏不受守卫限制(守卫只作用于 flush),pauseHistory/撤销期间产生的标记靠下次 flush 的字符串比对自愈
2822
+ // 对外变更版本号:PptEditor 侧的对外同步 watch 用它替代整册 deep watch(后者每次拖拽/打字
2823
+ // 都要 O(整册) 深遍历重收集依赖,页数越多越卡)。任何变更点 bump 一次即可
2824
+ const slidesRevision = ref(0)
2825
+
2805
2826
  function ensurePageWatchers() {
2806
2827
  const n = slides.value.length
2807
2828
  while (pageWatchers.length > n) pageWatchers.pop()!()
@@ -2812,6 +2833,7 @@ function ensurePageWatchers() {
2812
2833
  // 此窗口内还原路径已同步 currentPageStrs,无需标脏。watcher 回调先于 nextTick 复位执行,守卫可靠
2813
2834
  if (isUndoingRedoing.value) return
2814
2835
  dirtyPageIndexes.add(i)
2836
+ slidesRevision.value++
2815
2837
  scheduleHistorySnapshot()
2816
2838
  }, { deep: true }))
2817
2839
  }
@@ -2820,6 +2842,7 @@ function ensurePageWatchers() {
2820
2842
  // 长度哨兵:纯尾部追加(末页后 addSlide)不触发任何 index getter,必须靠它补注册 watcher
2821
2843
  watch(() => slides.value.length, () => {
2822
2844
  ensurePageWatchers()
2845
+ slidesRevision.value++
2823
2846
  scheduleHistorySnapshot()
2824
2847
  })
2825
2848
 
@@ -2861,6 +2884,7 @@ function ensureMasterWatchers() {
2861
2884
  masterWatchers.push(watch(() => slideMasters.value[i], () => {
2862
2885
  if (isUndoingRedoing.value) return
2863
2886
  dirtyMasterIndexes.add(i)
2887
+ slidesRevision.value++
2864
2888
  scheduleMasterHistorySnapshot()
2865
2889
  }, { deep: true }))
2866
2890
  }
@@ -2868,6 +2892,7 @@ function ensureMasterWatchers() {
2868
2892
 
2869
2893
  watch(() => slideMasters.value.length, () => {
2870
2894
  ensureMasterWatchers()
2895
+ slidesRevision.value++
2871
2896
  scheduleMasterHistorySnapshot()
2872
2897
  })
2873
2898
 
@@ -2918,6 +2943,7 @@ const canRedo = computed(() => isMasterEditingView.value
2918
2943
  // 按页还原:只解析与当前状态字符串不同的页,未变页对象引用复用(缩略图/组件 vnode 稳定)。
2919
2944
  // 始终返回新数组:依赖 slides.value 数组 identity 的 watcher(缩略图测量/滚动重置等)照常触发
2920
2945
  function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list: T[]): { list: T[]; changed: boolean } {
2946
+ slidesRevision.value++ // undo/redo 窗口期页 watcher 被守卫跳过,还原本身补一次 bump 供对外同步
2921
2947
  if (entryStrs.length !== list.length || entryStrs.length !== currentStrs.length) {
2922
2948
  // 结构性差异(增删页):与旧行为一致,整册逐页解析
2923
2949
  return { list: entryStrs.map(s => parsePage<T>(s)), changed: true }
@@ -3254,47 +3280,288 @@ function pasteSlideData(targetSlideData: any, afterIndex = activeIndex.value, is
3254
3280
  }
3255
3281
  }
3256
3282
 
3257
- function pasteElementsData(elementsArray: any[]) {
3258
- if (!activeSlide.value || !elementsArray || elementsArray.length === 0) return
3283
+ function copySelectedElements(): { type: 'elements'; data: SlideElement[]; sourceWidth: number; sourceHeight: number } | null {
3284
+ if (!activeSlide.value || !activeSlide.value.elements) return null
3285
+ if (selectedElementIds.value.length === 0) return null
3286
+
3287
+ const targetElements: SlideElement[] = []
3288
+ const selectedSet = new Set(selectedElementIds.value)
3289
+
3290
+ function isAnyAncestorSelected(id: string): boolean {
3291
+ let parent = findParentGroupInSlide(id, activeSlide.value!.elements)
3292
+ while (parent) {
3293
+ if (selectedSet.has(parent.id)) return true
3294
+ parent = findParentGroupInSlide(parent.id, activeSlide.value!.elements)
3295
+ }
3296
+ return false
3297
+ }
3298
+
3299
+ function getAbsoluteCanvasBox(el: SlideElement): { left: number; top: number; width: number; height: number } {
3300
+ const parentGroups: SlideElement[] = []
3301
+ let curr = findParentGroupInSlide(el.id, activeSlide.value!.elements)
3302
+ while (curr) {
3303
+ parentGroups.unshift(curr)
3304
+ curr = findParentGroupInSlide(curr.id, activeSlide.value!.elements)
3305
+ }
3306
+
3307
+ let box = {
3308
+ left: parseValToPx(el.style?.left, logicalWidth.value, (el as any).left || 0),
3309
+ top: parseValToPx(el.style?.top, logicalHeight.value, (el as any).top || 0),
3310
+ width: parseValToPx(el.style?.width, logicalWidth.value, (el as any).width || 200),
3311
+ height: parseValToPx(el.style?.height, logicalHeight.value, (el as any).height || 100)
3312
+ }
3313
+
3314
+ if (parentGroups.length === 0) return box
3315
+
3316
+ let pLeft = 0
3317
+ let pTop = 0
3318
+ let pWidth = logicalWidth.value
3319
+ let pHeight = logicalHeight.value
3320
+
3321
+ for (const pg of parentGroups) {
3322
+ const gLeft = parseValToPx(pg.style?.left, pWidth, (pg as any).left || 0)
3323
+ const gTop = parseValToPx(pg.style?.top, pHeight, (pg as any).top || 0)
3324
+ const gWidth = parseValToPx(pg.style?.width, pWidth, (pg as any).width || 200)
3325
+ const gHeight = parseValToPx(pg.style?.height, pHeight, (pg as any).height || 100)
3326
+ pLeft += gLeft
3327
+ pTop += gTop
3328
+ pWidth = gWidth
3329
+ pHeight = gHeight
3330
+ }
3331
+
3332
+ const relLeftPx = parseValToPx(el.style?.left, pWidth, (el as any).left || 0)
3333
+ const relTopPx = parseValToPx(el.style?.top, pHeight, (el as any).top || 0)
3334
+ const elWidthPx = parseValToPx(el.style?.width, pWidth, (el as any).width || 200)
3335
+ const elHeightPx = parseValToPx(el.style?.height, pHeight, (el as any).height || 50)
3336
+
3337
+ return {
3338
+ left: pLeft + relLeftPx,
3339
+ top: pTop + relTopPx,
3340
+ width: elWidthPx,
3341
+ height: elHeightPx
3342
+ }
3343
+ }
3344
+
3345
+ function findElementById(id: string, list: SlideElement[]): SlideElement | null {
3346
+ for (const item of list) {
3347
+ if (item.id === id) return item
3348
+ if (item.type === 'group' && Array.isArray(item.elements)) {
3349
+ const found = findElementById(id, item.elements)
3350
+ if (found) return found
3351
+ }
3352
+ }
3353
+ return null
3354
+ }
3355
+
3356
+ selectedElementIds.value.forEach(id => {
3357
+ if (isAnyAncestorSelected(id)) {
3358
+ return
3359
+ }
3360
+
3361
+ const el = findElementById(id, activeSlide.value!.elements)
3362
+ if (!el) return
3363
+
3364
+ const parentGroup = findParentGroupInSlide(id, activeSlide.value!.elements)
3365
+ if (parentGroup) {
3366
+ const box = getAbsoluteCanvasBox(el)
3367
+ const childClone = JSON.parse(JSON.stringify(el)) as SlideElement
3368
+ delete (childClone as any).groupId
3369
+ if (!childClone.style) childClone.style = {} as any
3370
+ childClone.style.left = `${((box.left / logicalWidth.value) * 100).toFixed(4)}%`
3371
+ childClone.style.top = `${((box.top / logicalHeight.value) * 100).toFixed(4)}%`
3372
+ childClone.style.width = `${((box.width / logicalWidth.value) * 100).toFixed(4)}%`
3373
+ childClone.style.height = `${((box.height / logicalHeight.value) * 100).toFixed(4)}%`
3374
+ ;(childClone as any).left = box.left
3375
+ ;(childClone as any).top = box.top
3376
+ ;(childClone as any).width = box.width
3377
+ ;(childClone as any).height = box.height
3378
+
3379
+ if (childClone.type === 'line') {
3380
+ const dX = box.left
3381
+ const dY = box.top
3382
+ if (childClone.startPoint && Array.isArray(childClone.startPoint)) childClone.startPoint = [childClone.startPoint[0] + dX, childClone.startPoint[1] + dY]
3383
+ if (childClone.endPoint && Array.isArray(childClone.endPoint)) childClone.endPoint = [childClone.endPoint[0] + dX, childClone.endPoint[1] + dY]
3384
+ }
3385
+ targetElements.push(childClone)
3386
+ } else {
3387
+ targetElements.push(JSON.parse(JSON.stringify(el)))
3388
+ }
3389
+ })
3390
+
3391
+ if (targetElements.length === 0) return null
3392
+
3393
+ clipboard.value = targetElements
3394
+ slideClipboard.value = null
3395
+
3396
+ return {
3397
+ type: 'elements',
3398
+ data: targetElements,
3399
+ sourceWidth: logicalWidth.value || 1280,
3400
+ sourceHeight: logicalHeight.value || 720
3401
+ }
3402
+ }
3403
+
3404
+ function scaleElementIfNeeded(el: any, sX: number, sY: number) {
3405
+ if (Math.abs(sX - 1) < 0.001 && Math.abs(sY - 1) < 0.001) return
3406
+
3407
+ if (el.style) {
3408
+ if (typeof el.style.left === 'string' && el.style.left.endsWith('px')) {
3409
+ el.style.left = `${(parseFloat(el.style.left) * sX).toFixed(1)}px`
3410
+ }
3411
+ if (typeof el.style.top === 'string' && el.style.top.endsWith('px')) {
3412
+ el.style.top = `${(parseFloat(el.style.top) * sY).toFixed(1)}px`
3413
+ }
3414
+ if (typeof el.style.width === 'string' && el.style.width.endsWith('px')) {
3415
+ el.style.width = `${(parseFloat(el.style.width) * sX).toFixed(1)}px`
3416
+ }
3417
+ if (typeof el.style.height === 'string' && el.style.height.endsWith('px')) {
3418
+ el.style.height = `${(parseFloat(el.style.height) * sY).toFixed(1)}px`
3419
+ }
3420
+ if (typeof el.style.fontSize === 'string' && el.style.fontSize.endsWith('px')) {
3421
+ el.style.fontSize = `${(parseFloat(el.style.fontSize) * ((sX + sY) / 2)).toFixed(1)}px`
3422
+ }
3423
+ }
3424
+
3425
+ if (typeof el.left === 'number') el.left = +(el.left * sX).toFixed(1)
3426
+ if (typeof el.top === 'number') el.top = +(el.top * sY).toFixed(1)
3427
+ if (typeof el.width === 'number') el.width = +(el.width * sX).toFixed(1)
3428
+ if (typeof el.height === 'number') el.height = +(el.height * sY).toFixed(1)
3429
+
3430
+ if (el.type === 'line') {
3431
+ if (el.startPoint && Array.isArray(el.startPoint)) el.startPoint = [el.startPoint[0] * sX, el.startPoint[1] * sY]
3432
+ if (el.endPoint && Array.isArray(el.endPoint)) el.endPoint = [el.endPoint[0] * sX, el.endPoint[1] * sY]
3433
+ }
3434
+
3435
+ if (el.type === 'group' && Array.isArray(el.elements)) {
3436
+ el.elements.forEach((child: any) => {
3437
+ if (child.style && typeof child.style.fontSize === 'string' && child.style.fontSize.endsWith('px')) {
3438
+ child.style.fontSize = `${(parseFloat(child.style.fontSize) * ((sX + sY) / 2)).toFixed(1)}px`
3439
+ }
3440
+ if (child.type === 'group') {
3441
+ scaleElementIfNeeded(child, sX, sY)
3442
+ }
3443
+ })
3444
+ }
3445
+ }
3446
+
3447
+ function pasteElementsData(elementsArray: any[], options?: { sourceWidth?: number; sourceHeight?: number }) {
3448
+ if (!activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
3259
3449
  const pastedIds: string[] = []
3450
+ const oldIdToNewId = new Map<string, string>()
3260
3451
  const oldGroupIdToNewGroupId = new Map<string, string>()
3261
3452
 
3453
+ const destW = logicalWidth.value || 1280
3454
+ const destH = logicalHeight.value || 720
3455
+ const srcW = (options && options.sourceWidth && options.sourceWidth > 0) ? options.sourceWidth : destW
3456
+ const srcH = (options && options.sourceHeight && options.sourceHeight > 0) ? options.sourceHeight : destH
3457
+ const scaleX = destW / srcW
3458
+ const scaleY = destH / srcH
3459
+ const needScale = Math.abs(scaleX - 1) > 0.001 || Math.abs(scaleY - 1) > 0.001
3460
+
3461
+ function reassignIdsRecursively(item: any) {
3462
+ const oldId = item.id
3463
+ const prefix = (oldId && typeof oldId === 'string' && oldId.startsWith('title')) ? 'title-' : ''
3464
+ const newId = prefix + Date.now().toString(36) + Math.random().toString(36).substring(2, 7)
3465
+ if (oldId) {
3466
+ oldIdToNewId.set(oldId, newId)
3467
+ }
3468
+ item.id = newId
3469
+
3470
+ if (item.groupId) {
3471
+ if (!oldGroupIdToNewGroupId.has(item.groupId)) {
3472
+ oldGroupIdToNewGroupId.set(item.groupId, 'group_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 7))
3473
+ }
3474
+ item.groupId = oldGroupIdToNewGroupId.get(item.groupId)
3475
+ }
3476
+
3477
+ if (item.type === 'group' && Array.isArray(item.elements)) {
3478
+ item.elements.forEach(reassignIdsRecursively)
3479
+ }
3480
+ }
3481
+
3482
+ function remapLineConnections(item: any) {
3483
+ if (item.type === 'line') {
3484
+ if (item.startConnection && item.startConnection.id) {
3485
+ if (oldIdToNewId.has(item.startConnection.id)) {
3486
+ item.startConnection.id = oldIdToNewId.get(item.startConnection.id)!
3487
+ } else {
3488
+ delete item.startConnection
3489
+ }
3490
+ }
3491
+ if (item.endConnection && item.endConnection.id) {
3492
+ if (oldIdToNewId.has(item.endConnection.id)) {
3493
+ item.endConnection.id = oldIdToNewId.get(item.endConnection.id)!
3494
+ } else {
3495
+ delete item.endConnection
3496
+ }
3497
+ }
3498
+ if (item.startElementId) {
3499
+ if (oldIdToNewId.has(item.startElementId)) {
3500
+ item.startElementId = oldIdToNewId.get(item.startElementId)
3501
+ } else {
3502
+ delete item.startElementId
3503
+ }
3504
+ }
3505
+ if (item.endElementId) {
3506
+ if (oldIdToNewId.has(item.endElementId)) {
3507
+ item.endElementId = oldIdToNewId.get(item.endElementId)
3508
+ } else {
3509
+ delete item.endElementId
3510
+ }
3511
+ }
3512
+ }
3513
+ if (item.type === 'group' && Array.isArray(item.elements)) {
3514
+ item.elements.forEach(remapLineConnections)
3515
+ }
3516
+ }
3517
+
3518
+ const offsetPx = 20
3519
+
3262
3520
  elementsArray.forEach((el: any) => {
3263
3521
  const newEl = JSON.parse(JSON.stringify(el))
3264
- newEl.id = (el.id && el.id.startsWith('title') ? 'title-' : '') + Date.now().toString() + Math.random().toString(36).substring(2, 7)
3265
- if (newEl.groupId) {
3266
- if (!oldGroupIdToNewGroupId.has(newEl.groupId)) {
3267
- oldGroupIdToNewGroupId.set(newEl.groupId, 'group_' + Date.now().toString() + Math.random().toString(36).substring(2, 7))
3268
- }
3269
- newEl.groupId = oldGroupIdToNewGroupId.get(newEl.groupId)
3522
+ reassignIdsRecursively(newEl)
3523
+ remapLineConnections(newEl)
3524
+
3525
+ if (needScale) {
3526
+ scaleElementIfNeeded(newEl, scaleX, scaleY)
3270
3527
  }
3271
3528
 
3272
3529
  if (newEl.style && newEl.style.left) {
3273
3530
  const leftVal = parseFloat(newEl.style.left) || 0
3274
- const isPx = newEl.style.left.endsWith('px')
3275
3531
  const isPct = newEl.style.left.endsWith('%')
3276
- if (isPx) {
3277
- newEl.style.left = `${leftVal + 20}px`
3278
- } else if (isPct) {
3279
- newEl.style.left = `${(leftVal + (20 / (logicalWidth.value || 1280)) * 100).toFixed(2)}%`
3532
+ if (isPct) {
3533
+ newEl.style.left = `${(leftVal + (offsetPx / destW) * 100).toFixed(4)}%`
3280
3534
  } else {
3281
- newEl.style.left = `${leftVal + 20}px`
3535
+ newEl.style.left = `${(leftVal + offsetPx).toFixed(1)}px`
3282
3536
  }
3283
3537
  }
3284
3538
  if (newEl.style && newEl.style.top) {
3285
3539
  const topVal = parseFloat(newEl.style.top) || 0
3286
- const isPx = newEl.style.top.endsWith('px')
3287
3540
  const isPct = newEl.style.top.endsWith('%')
3288
- if (isPx) {
3289
- newEl.style.top = `${topVal + 20}px`
3290
- } else if (isPct) {
3291
- newEl.style.top = `${(topVal + (20 / (logicalHeight.value || 720)) * 100).toFixed(2)}%`
3541
+ if (isPct) {
3542
+ newEl.style.top = `${(topVal + (offsetPx / destH) * 100).toFixed(4)}%`
3292
3543
  } else {
3293
- newEl.style.top = `${topVal + 20}px`
3544
+ newEl.style.top = `${(topVal + offsetPx).toFixed(1)}px`
3294
3545
  }
3295
3546
  }
3296
3547
 
3297
- activeSlide.value.elements.push(newEl)
3548
+ if (typeof newEl.left === 'number') newEl.left += offsetPx
3549
+ if (typeof newEl.top === 'number') newEl.top += offsetPx
3550
+
3551
+ if (newEl.type === 'line') {
3552
+ if (newEl.startPoint && Array.isArray(newEl.startPoint)) {
3553
+ newEl.startPoint = [newEl.startPoint[0] + offsetPx, newEl.startPoint[1] + offsetPx]
3554
+ }
3555
+ if (newEl.endPoint && Array.isArray(newEl.endPoint)) {
3556
+ newEl.endPoint = [newEl.endPoint[0] + offsetPx, newEl.endPoint[1] + offsetPx]
3557
+ }
3558
+ }
3559
+
3560
+ if (newEl.type === 'group') {
3561
+ updateGroupBounds(newEl)
3562
+ }
3563
+
3564
+ activeSlide.value!.elements.push(newEl)
3298
3565
  pastedIds.push(newEl.id)
3299
3566
  })
3300
3567
 
@@ -3302,6 +3569,7 @@ function pasteElementsData(elementsArray: any[]) {
3302
3569
  if (pastedIds.length > 0) {
3303
3570
  selectedElementId.value = pastedIds[pastedIds.length - 1]
3304
3571
  }
3572
+ addHistorySnapshot()
3305
3573
  notify(`已粘贴 ${pastedIds.length} 个元素`)
3306
3574
  }
3307
3575
 
@@ -3411,6 +3679,10 @@ function duplicateSelectedElements(targetSlide = activeSlide.value) {
3411
3679
  targetSlide.elements.push(cloned)
3412
3680
  }
3413
3681
 
3682
+ if (cloned.type === 'group') {
3683
+ updateGroupBounds(cloned)
3684
+ }
3685
+
3414
3686
  newElementIds.push(cloned.id)
3415
3687
  clonedElements.push(cloned)
3416
3688
  })
@@ -3491,6 +3763,12 @@ async function handleUnifiedPaste(afterSlideIndex?: number) {
3491
3763
  pasteSlideData(data.data, afterSlideIndex !== undefined ? afterSlideIndex : activeIndex.value)
3492
3764
  return true
3493
3765
  }
3766
+ if (data && data.type === 'elements' && Array.isArray(data.data) && data.data.length > 0) {
3767
+ clipboard.value = data.data
3768
+ slideClipboard.value = null
3769
+ pasteElementsData(data.data, { sourceWidth: data.sourceWidth, sourceHeight: data.sourceHeight })
3770
+ return true
3771
+ }
3494
3772
  if (Array.isArray(data) && data.length > 0 && data[0].type && data[0].style) {
3495
3773
  clipboard.value = data
3496
3774
  slideClipboard.value = null
@@ -4209,36 +4487,20 @@ function combineElements() {
4209
4487
  const idx = activeSlide.value!.elements.indexOf(el)
4210
4488
  if (idx > maxZIndex) maxZIndex = idx
4211
4489
 
4212
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4213
- const left = parseValToPx(el.style?.left, logicalWidth.value, 0)
4214
- const top = parseValToPx(el.style?.top, logicalHeight.value, 0)
4215
- let width = parseValToPx(el.style?.width, logicalWidth.value, 200)
4216
- let height = parseValToPx(el.style?.height, logicalHeight.value, 50)
4217
-
4218
- if (domEl) {
4219
- width = domEl.offsetWidth || width
4220
- height = domEl.offsetHeight || height
4221
- }
4222
-
4223
- if (width <= 0) width = 100
4224
- if (height <= 0) height = 30
4490
+ const rawLeft = el.style?.left ?? (typeof (el as any).left === 'number' ? `${(el as any).left}px` : undefined)
4491
+ const rawTop = el.style?.top ?? (typeof (el as any).top === 'number' ? `${(el as any).top}px` : undefined)
4492
+ const rawWidth = el.style?.width ?? (typeof (el as any).width === 'number' ? `${(el as any).width}px` : undefined)
4493
+ const rawHeight = el.style?.height ?? (typeof (el as any).height === 'number' ? `${(el as any).height}px` : undefined)
4225
4494
 
4226
- if (left < minLeft) minLeft = left
4227
- if (top < minTop) minTop = top
4228
- if (left + width > maxRight) maxRight = left + width
4229
- if (top + height > maxBottom) maxBottom = top + height
4230
- })
4495
+ const left = parseValToPx(rawLeft, logicalWidth.value, 0)
4496
+ const top = parseValToPx(rawTop, logicalHeight.value, 0)
4497
+ let width = parseValToPx(rawWidth, logicalWidth.value, 100)
4498
+ let height = parseValToPx(rawHeight, logicalHeight.value, 40)
4231
4499
 
4232
- targetElements.forEach(el => {
4233
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4234
- const left = parseValToPx(el.style?.left, logicalWidth.value, 0)
4235
- const top = parseValToPx(el.style?.top, logicalHeight.value, 0)
4236
- let width = parseValToPx(el.style?.width, logicalWidth.value, 200)
4237
- let height = parseValToPx(el.style?.height, logicalHeight.value, 50)
4238
-
4239
- if (domEl) {
4240
- width = domEl.offsetWidth || width
4241
- height = domEl.offsetHeight || height
4500
+ const domEl = typeof document !== 'undefined' ? document.getElementById(`canvas-el-${el.id}`) : null
4501
+ if (domEl && el.type === 'text') {
4502
+ if (domEl.offsetHeight > 0) height = domEl.offsetHeight
4503
+ if (el.style?.width === 'auto' && domEl.offsetWidth > 0) width = domEl.offsetWidth
4242
4504
  }
4243
4505
 
4244
4506
  if (width <= 0) width = 100
@@ -4256,15 +4518,21 @@ function combineElements() {
4256
4518
  // Convert each target element's coordinates relative to the group container (%)
4257
4519
  const childElements: SlideElement[] = targetElements.map(el => {
4258
4520
  const childClone = JSON.parse(JSON.stringify(el)) as SlideElement
4259
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4260
- const elLeft = parseValToPx(el.style?.left, logicalWidth.value, 0)
4261
- const elTop = parseValToPx(el.style?.top, logicalHeight.value, 0)
4262
- let elWidth = parseValToPx(el.style?.width, logicalWidth.value, 200)
4263
- let elHeight = parseValToPx(el.style?.height, logicalHeight.value, 50)
4264
4521
 
4265
- if (domEl) {
4266
- elWidth = domEl.offsetWidth || elWidth
4267
- elHeight = domEl.offsetHeight || elHeight
4522
+ const rawLeft = el.style?.left ?? (typeof (el as any).left === 'number' ? `${(el as any).left}px` : undefined)
4523
+ const rawTop = el.style?.top ?? (typeof (el as any).top === 'number' ? `${(el as any).top}px` : undefined)
4524
+ const rawWidth = el.style?.width ?? (typeof (el as any).width === 'number' ? `${(el as any).width}px` : undefined)
4525
+ const rawHeight = el.style?.height ?? (typeof (el as any).height === 'number' ? `${(el as any).height}px` : undefined)
4526
+
4527
+ const elLeft = parseValToPx(rawLeft, logicalWidth.value, 0)
4528
+ const elTop = parseValToPx(rawTop, logicalHeight.value, 0)
4529
+ let elWidth = parseValToPx(rawWidth, logicalWidth.value, 100)
4530
+ let elHeight = parseValToPx(rawHeight, logicalHeight.value, 40)
4531
+
4532
+ const domEl = typeof document !== 'undefined' ? document.getElementById(`canvas-el-${el.id}`) : null
4533
+ if (domEl && el.type === 'text') {
4534
+ if (domEl.offsetHeight > 0) elHeight = domEl.offsetHeight
4535
+ if (el.style?.width === 'auto' && domEl.offsetWidth > 0) elWidth = domEl.offsetWidth
4268
4536
  }
4269
4537
 
4270
4538
  const relLeft = ((elLeft - minLeft) / groupWidth) * 100
@@ -4278,6 +4546,20 @@ function combineElements() {
4278
4546
  childClone.style.width = `${relWidth.toFixed(4)}%`
4279
4547
  childClone.style.height = `${relHeight.toFixed(4)}%`
4280
4548
 
4549
+ ;(childClone as any).left = elLeft - minLeft
4550
+ ;(childClone as any).top = elTop - minTop
4551
+ ;(childClone as any).width = elWidth
4552
+ ;(childClone as any).height = elHeight
4553
+
4554
+ if (childClone.type === 'line') {
4555
+ if (childClone.startPoint && Array.isArray(childClone.startPoint)) {
4556
+ childClone.startPoint = [childClone.startPoint[0] - minLeft, childClone.startPoint[1] - minTop]
4557
+ }
4558
+ if (childClone.endPoint && Array.isArray(childClone.endPoint)) {
4559
+ childClone.endPoint = [childClone.endPoint[0] - minLeft, childClone.endPoint[1] - minTop]
4560
+ }
4561
+ }
4562
+
4281
4563
  return childClone
4282
4564
  })
4283
4565
 
@@ -4295,6 +4577,10 @@ function combineElements() {
4295
4577
  height: `${((groupHeight / logicalHeight.value) * 100).toFixed(4)}%`,
4296
4578
  rotate: 0
4297
4579
  },
4580
+ left: minLeft,
4581
+ top: minTop,
4582
+ width: groupWidth,
4583
+ height: groupHeight,
4298
4584
  elements: childElements
4299
4585
  }
4300
4586
 
@@ -4367,6 +4653,22 @@ function uncombineElement(groupId?: string) {
4367
4653
  childClone.style.width = `${((absWidthPx / logicalWidth.value) * 100).toFixed(4)}%`
4368
4654
  childClone.style.height = `${((absHeightPx / logicalHeight.value) * 100).toFixed(4)}%`
4369
4655
 
4656
+ ;(childClone as any).left = absLeftPx
4657
+ ;(childClone as any).top = absTopPx
4658
+ ;(childClone as any).width = absWidthPx
4659
+ ;(childClone as any).height = absHeightPx
4660
+
4661
+ if (childClone.type === 'line') {
4662
+ const dX = groupLeft
4663
+ const dY = groupTop
4664
+ if (childClone.startPoint && Array.isArray(childClone.startPoint)) {
4665
+ childClone.startPoint = [childClone.startPoint[0] + dX, childClone.startPoint[1] + dY]
4666
+ }
4667
+ if (childClone.endPoint && Array.isArray(childClone.endPoint)) {
4668
+ childClone.endPoint = [childClone.endPoint[0] + dX, childClone.endPoint[1] + dY]
4669
+ }
4670
+ }
4671
+
4370
4672
  return childClone
4371
4673
  })
4372
4674
 
@@ -4488,10 +4790,10 @@ function updateGroupBounds(groupEl: SlideElement) {
4488
4790
  groupEl.style.width = isWidthPx ? `${newGroupWidth.toFixed(1)}px` : `${((newGroupWidth / parentDim.width) * 100).toFixed(4)}%`
4489
4791
  groupEl.style.height = isHeightPx ? `${newGroupHeight.toFixed(1)}px` : `${((newGroupHeight / parentDim.height) * 100).toFixed(4)}%`
4490
4792
 
4491
- if (typeof (groupEl as any).left === 'number') (groupEl as any).left = minAbsLeft
4492
- if (typeof (groupEl as any).top === 'number') (groupEl as any).top = minAbsTop
4493
- if (typeof (groupEl as any).width === 'number') (groupEl as any).width = newGroupWidth
4494
- if (typeof (groupEl as any).height === 'number') (groupEl as any).height = newGroupHeight
4793
+ ;(groupEl as any).left = minAbsLeft
4794
+ ;(groupEl as any).top = minAbsTop
4795
+ ;(groupEl as any).width = newGroupWidth
4796
+ ;(groupEl as any).height = newGroupHeight
4495
4797
 
4496
4798
  absChildren.forEach(({ child, absLeft, absTop, absWidth, absHeight }) => {
4497
4799
  const newRelLeftPct = ((absLeft - minAbsLeft) / newGroupWidth) * 100
@@ -4505,10 +4807,10 @@ function updateGroupBounds(groupEl: SlideElement) {
4505
4807
  child.style.width = `${newRelWidthPct.toFixed(4)}%`
4506
4808
  child.style.height = `${newRelHeightPct.toFixed(4)}%`
4507
4809
 
4508
- if (typeof (child as any).left === 'number') (child as any).left = absLeft - minAbsLeft
4509
- if (typeof (child as any).top === 'number') (child as any).top = absTop - minAbsTop
4510
- if (typeof (child as any).width === 'number') (child as any).width = absWidth
4511
- if (typeof (child as any).height === 'number') (child as any).height = absHeight
4810
+ ;(child as any).left = absLeft - minAbsLeft
4811
+ ;(child as any).top = absTop - minAbsTop
4812
+ ;(child as any).width = absWidth
4813
+ ;(child as any).height = absHeight
4512
4814
  })
4513
4815
 
4514
4816
  // Recursive parent group bounds update if nested in another group
@@ -4548,6 +4850,8 @@ function deleteElement(id: string, silent = false) {
4548
4850
  const currentAnimationIndex = ref(0)
4549
4851
  const isPlayingAnimation = ref(false)
4550
4852
  const playedTriggerAnimations = ref<string[]>([])
4853
+ // 动画代际:快速跳步/换页/退出放映时 +1,使挂起的 autoNext setTimeout 链失效(防止旧链在新页继续跑)
4854
+ const animationGeneration = ref(0)
4551
4855
 
4552
4856
  function getFormatedAnimationsForSlide(slide: Slide) {
4553
4857
  if (!slide.animations) return []
@@ -4643,7 +4947,9 @@ function runAnimationStep() {
4643
4947
  if (finishedCount === animations.length) {
4644
4948
  isPlayingAnimation.value = false
4645
4949
  if (autoNext) {
4950
+ const gen = animationGeneration.value
4646
4951
  setTimeout(() => {
4952
+ if (gen !== animationGeneration.value) return
4647
4953
  runAnimationStep()
4648
4954
  }, 50)
4649
4955
  }
@@ -4669,36 +4975,6 @@ function runAnimationStep() {
4669
4975
  }
4670
4976
  }
4671
4977
 
4672
- function revokeAnimationStep() {
4673
- if (currentAnimationIndex.value <= 0) return
4674
-
4675
- currentAnimationIndex.value -= 1
4676
- const formatted = formatedAnimations.value
4677
- const { animations } = formatted[currentAnimationIndex.value]
4678
-
4679
- for (const animation of animations) {
4680
- const el = document.getElementById(`screen-element-${animation.elId}`)
4681
- if (!el) continue
4682
-
4683
- el.style.animationDuration = ''
4684
- el.style.animationDelay = ''
4685
- el.style.animationIterationCount = ''
4686
- el.style.removeProperty('--animate-duration')
4687
- el.style.removeProperty('--animate-repeat')
4688
- el.style.removeProperty('visibility')
4689
- for (const cls of Array.from(el.classList)) {
4690
- if (cls.startsWith('animate__')) {
4691
- el.classList.remove(cls)
4692
- }
4693
- }
4694
- el.classList.remove('animate__animated')
4695
- }
4696
-
4697
- if (animations.every(item => item.type === 'attention')) {
4698
- revokeAnimationStep()
4699
- }
4700
- }
4701
-
4702
4978
  function runTriggerAnimations(clickedElId: string) {
4703
4979
  if (!activeSlide.value || !activeSlide.value.animations) return
4704
4980
  const elIds = activeSlide.value.elements.map(el => el.id)
@@ -4768,11 +5044,62 @@ function restoreAnimationState(slide: Slide, targetIndex: number) {
4768
5044
  }
4769
5045
  }
4770
5046
 
5047
+ // 纯自动步:全部动画都是 auto / meantime 触发(不消耗点击)
5048
+ function isAutoAnimationStep(step: FormatedAnimation): boolean {
5049
+ return step.animations.every(a => a.trigger === 'auto' || a.trigger === 'meantime')
5050
+ }
5051
+
5052
+ function clearElementAnimationState(el: HTMLElement) {
5053
+ el.style.animationDuration = ''
5054
+ el.style.animationDelay = ''
5055
+ el.style.animationIterationCount = ''
5056
+ el.style.removeProperty('--animate-duration')
5057
+ el.style.removeProperty('--animate-repeat')
5058
+ el.style.removeProperty('visibility')
5059
+ for (const cls of Array.from(el.classList)) {
5060
+ if (cls.startsWith('animate__')) {
5061
+ el.classList.remove(cls)
5062
+ }
5063
+ }
5064
+ el.classList.remove('animate__animated')
5065
+ }
5066
+
5067
+ // 瞬间补齐 [currentAnimationIndex, target) 的动画步(不播动画,元素直接就位),并作废挂起的自动链
5068
+ function completeAnimationStepsTo(target: number) {
5069
+ const formatted = formatedAnimations.value
5070
+ for (let i = currentAnimationIndex.value; i < target && i < formatted.length; i++) {
5071
+ for (const animation of formatted[i].animations) {
5072
+ const el = document.getElementById(`screen-element-${animation.elId}`)
5073
+ if (!el) continue
5074
+ clearElementAnimationState(el)
5075
+ if (animation.type === 'out') el.style.visibility = 'hidden'
5076
+ }
5077
+ }
5078
+ animationGeneration.value++
5079
+ isPlayingAnimation.value = false
5080
+ currentAnimationIndex.value = Math.min(target, formatted.length)
5081
+ }
5082
+
5083
+ // 撤销 [target, currentAnimationIndex) 的动画步:in→隐藏、out→恢复可见,并作废挂起的自动链
5084
+ function revokeAnimationStepsTo(target: number) {
5085
+ const formatted = formatedAnimations.value
5086
+ for (let i = target; i < currentAnimationIndex.value && i < formatted.length; i++) {
5087
+ for (const animation of formatted[i].animations) {
5088
+ const el = document.getElementById(`screen-element-${animation.elId}`)
5089
+ if (!el) continue
5090
+ clearElementAnimationState(el)
5091
+ if (animation.type === 'in') el.style.visibility = 'hidden'
5092
+ else if (animation.type === 'out') el.style.removeProperty('visibility')
5093
+ }
5094
+ }
5095
+ animationGeneration.value++
5096
+ isPlayingAnimation.value = false
5097
+ currentAnimationIndex.value = Math.max(0, target)
5098
+ }
5099
+
4771
5100
  function execNext() {
4772
5101
  const formatted = formatedAnimations.value
4773
- if (formatted.length && currentAnimationIndex.value < formatted.length) {
4774
- runAnimationStep()
4775
- } else {
5102
+ const goNextSlide = () => {
4776
5103
  if (activeIndex.value < slides.value.length - 1) {
4777
5104
  activeIndex.value++
4778
5105
  currentAnimationIndex.value = 0
@@ -4781,22 +5108,42 @@ function execNext() {
4781
5108
  notify('演示已结束')
4782
5109
  }
4783
5110
  }
5111
+ if (formatted.length && currentAnimationIndex.value < formatted.length) {
5112
+ // 连续的纯自动步不消耗点击:瞬间补齐,停到下一个点击步或本页末尾(PPT 行为)
5113
+ let target = currentAnimationIndex.value
5114
+ while (target < formatted.length && isAutoAnimationStep(formatted[target])) target++
5115
+ if (target > currentAnimationIndex.value) {
5116
+ completeAnimationStepsTo(target)
5117
+ }
5118
+ if (currentAnimationIndex.value < formatted.length) {
5119
+ runAnimationStep()
5120
+ } else {
5121
+ goNextSlide()
5122
+ }
5123
+ } else {
5124
+ goNextSlide()
5125
+ }
4784
5126
  }
4785
5127
 
4786
5128
  function execPrev() {
4787
- if (currentAnimationIndex.value > 0) {
4788
- revokeAnimationStep()
4789
- } else {
4790
- if (activeIndex.value > 0) {
4791
- activeIndex.value--
4792
- const prevSlide = slides.value[activeIndex.value]
4793
- if (prevSlide) {
4794
- const formatted = getFormatedAnimationsForSlide(prevSlide)
4795
- currentAnimationIndex.value = formatted.length
4796
- nextTick(() => {
4797
- restoreAnimationState(prevSlide, currentAnimationIndex.value)
4798
- })
4799
- }
5129
+ const formatted = formatedAnimations.value
5130
+ // 回退只落在"点击步"上:纯自动步不占点击;已播步里没有点击步时直接退回上一页
5131
+ let target = currentAnimationIndex.value
5132
+ while (target > 0 && isAutoAnimationStep(formatted[target - 1])) target--
5133
+ if (target > 0) {
5134
+ revokeAnimationStepsTo(target - 1)
5135
+ return
5136
+ }
5137
+ // target===0:已播步全是自动步(不占点击)→ 视同本页无点击交互,直接退回上一页
5138
+ if (activeIndex.value > 0) {
5139
+ activeIndex.value--
5140
+ const prevSlide = slides.value[activeIndex.value]
5141
+ if (prevSlide) {
5142
+ const prevFormatted = getFormatedAnimationsForSlide(prevSlide)
5143
+ currentAnimationIndex.value = prevFormatted.length
5144
+ nextTick(() => {
5145
+ restoreAnimationState(prevSlide, currentAnimationIndex.value)
5146
+ })
4800
5147
  }
4801
5148
  }
4802
5149
  }
@@ -4850,6 +5197,7 @@ function needWaitAnimation(slide: Slide, elId: string, animationIndex: number) {
4850
5197
 
4851
5198
  watch([activeIndex, isPresenting], () => {
4852
5199
  playedTriggerAnimations.value = []
5200
+ animationGeneration.value++ // 换页/进出放映:作废挂起的 autoNext 自动链
4853
5201
  currentAnimationIndex.value = 0
4854
5202
  if (!isPresenting.value) return
4855
5203
 
@@ -5703,6 +6051,10 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5703
6051
  if ((el.wrap === 'none' || el.wrap === false) && (!el.style.whiteSpace || el.style.whiteSpace === 'normal')) {
5704
6052
  el.style.whiteSpace = 'nowrap'
5705
6053
  }
6054
+ const rawLh = el.style?.lineHeight || el.lineHeight || el.text?.lineHeight
6055
+ if (rawLh && !el.style.lineHeight) {
6056
+ el.style.lineHeight = rawLh
6057
+ }
5706
6058
  if (el.shadow) {
5707
6059
  if (isEffectiveLocalShadow(el.shadow)) {
5708
6060
  if (!el.style.boxShadow || el.style.boxShadow === 'none') {
@@ -5873,7 +6225,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5873
6225
 
5874
6226
  const normType = String(shapType).toLowerCase()
5875
6227
 
5876
- for (const group of PPTX_SHAPE_LIST) {
6228
+ for (const group of shapeList()) {
5877
6229
  for (const item of group.children) {
5878
6230
  const itemPptxType = (item.pptxShapeType || '').toLowerCase()
5879
6231
  const itemFormula = (item.pathFormula || '').toLowerCase()
@@ -5894,8 +6246,8 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5894
6246
  const matchedShapType = item.pptxShapeType || shapType
5895
6247
  const matchedPathFormula = item.pathFormula
5896
6248
 
5897
- if (item.pathFormula && PPTX_SHAPE_PATH_FORMULAS[item.pathFormula]) {
5898
- const formula = PPTX_SHAPE_PATH_FORMULAS[item.pathFormula]
6249
+ if (item.pathFormula && shapeFormulas()[item.pathFormula]) {
6250
+ const formula = shapeFormulas()[item.pathFormula]
5899
6251
  let kps = formula.defaultValue ? [...formula.defaultValue] : []
5900
6252
  if (el.keypoints) {
5901
6253
  if (Array.isArray(el.keypoints) && el.keypoints.length > 0) {
@@ -6648,7 +7000,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6648
7000
  let pFormula = el.pathFormula
6649
7001
  if (!pFormula && el.shapType) {
6650
7002
  const norm = String(el.shapType).toLowerCase()
6651
- let shape = PPTX_SHAPE_LIST.flatMap((x: any) => x.children).find((item: any) => {
7003
+ let shape = shapeList().flatMap((x: any) => x.children).find((item: any) => {
6652
7004
  const t1 = (item.pptxShapeType || '').toLowerCase()
6653
7005
  const t2 = (item.pathFormula || '').toLowerCase()
6654
7006
  return t1 === norm || t2 === norm ||
@@ -6663,7 +7015,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6663
7015
  if (shape) pFormula = shape.pathFormula
6664
7016
  }
6665
7017
  if (pFormula) {
6666
- const formulaObj = PPTX_SHAPE_PATH_FORMULAS[pFormula as string]
7018
+ const formulaObj = shapeFormulas()[pFormula as string]
6667
7019
  if (formulaObj && formulaObj.defaultValue) {
6668
7020
  const kps = el.keypoints && Array.isArray(el.keypoints) && el.keypoints.length > 0 ? [...el.keypoints] : []
6669
7021
  const defaults = formulaObj.defaultValue
@@ -6704,7 +7056,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6704
7056
  pathFormula: (() => {
6705
7057
  if (type === 'shape' && el.shapType && !el.pathFormula) {
6706
7058
  const norm = String(el.shapType).toLowerCase()
6707
- let shape = PPTX_SHAPE_LIST.flatMap((x: any) => x.children).find((item: any) => {
7059
+ let shape = shapeList().flatMap((x: any) => x.children).find((item: any) => {
6708
7060
  const t1 = (item.pptxShapeType || '').toLowerCase()
6709
7061
  const t2 = (item.pathFormula || '').toLowerCase()
6710
7062
  return t1 === norm || t2 === norm ||
@@ -7853,6 +8205,7 @@ async function aiReplaceCurrentSlide(options?: { primaryCategory?: string; secon
7853
8205
  notify('正在进行一键美化,替换当前幻灯片内容...')
7854
8206
 
7855
8207
  try {
8208
+ await ensurePptxShapes() // 后续 pptParserFn/convertPPTistToLocalIfNeed 同步消费形状表,先确保就绪
7856
8209
  const pureText = extractPureTextFromSlide(activeSlide.value)
7857
8210
  if (!pureText) {
7858
8211
  notify('当前幻灯片没有可提取的纯文字内容')
@@ -8891,6 +9244,7 @@ export function useSlides() {
8891
9244
  zoomRatio,
8892
9245
  fitTrigger,
8893
9246
  slides,
9247
+ slidesRevision,
8894
9248
  activeIndex,
8895
9249
  activeSlide,
8896
9250
  selectedSlideIndexes,
@@ -8971,6 +9325,7 @@ export function useSlides() {
8971
9325
  slideClipboard,
8972
9326
  pasteSlideData,
8973
9327
  pasteElementsData,
9328
+ copySelectedElements,
8974
9329
  handleUnifiedPaste,
8975
9330
  shapeFormatPainter,
8976
9331
  currentSlideAnimations,