jvs-ppt-ui 0.0.8 → 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.
Files changed (34) hide show
  1. package/README.md +60 -2
  2. package/dist/lib/jvs-ppt-ui.es.js +22404 -21454
  3. package/dist/lib/jvs-ppt-ui.umd.js +102 -101
  4. package/dist/lib/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/App.vue +53 -53
  7. package/src/components/AIPptDialog.vue +2 -0
  8. package/src/components/CanvasArea.vue +164 -72
  9. package/src/components/Header.vue +374 -144
  10. package/src/components/PptEditor.vue +33 -15
  11. package/src/components/PresentationMode.vue +24 -4
  12. package/src/components/SidebarRight.vue +8 -1
  13. package/src/components/SvgChart.vue +5 -2
  14. package/src/components/TemplateGallery.vue +1321 -866
  15. package/src/components/Toolbar.vue +244 -22
  16. package/src/components/canvas/CanvasTransformHandles.vue +5 -1
  17. package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
  18. package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
  19. package/src/components/canvas/textSelectionOverlay.ts +305 -0
  20. package/src/composables/canvas/useCanvasAlignment.ts +286 -238
  21. package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
  22. package/src/composables/canvas/useCanvasFormatting.ts +35 -12
  23. package/src/composables/canvas/useCanvasTable.ts +52 -24
  24. package/src/composables/canvas/useCanvasText.ts +12 -0
  25. package/src/composables/canvas/useCanvasTransform.ts +15 -2
  26. package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
  27. package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
  28. package/src/composables/useSlides.ts +495 -132
  29. package/src/style.css +88 -0
  30. package/src/types/index.ts +8 -0
  31. package/src/utils/line.ts +274 -312
  32. package/src/utils/pptxShapes.ts +1 -1
  33. package/src/utils/pptxShapesLazy.ts +39 -0
  34. 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
 
@@ -2568,6 +2576,18 @@ function markAllPagesDirty() {
2568
2576
  for (let i = 0; i < slides.value.length; i++) dirtyPageIndexes.add(i)
2569
2577
  }
2570
2578
 
2579
+ // 递归释放元素(含组合内子元素)的 blob: URL,防止导入替换后组内媒体泄漏
2580
+ function revokeElementBlobUrls(el: any) {
2581
+ if (!el) return
2582
+ if (Array.isArray(el.elements)) {
2583
+ for (const child of el.elements) revokeElementBlobUrls(child)
2584
+ }
2585
+ const src = el.src || el.content
2586
+ if (typeof src === 'string' && src.startsWith('blob:')) {
2587
+ try { URL.revokeObjectURL(src) } catch (e) {}
2588
+ }
2589
+ }
2590
+
2571
2591
  function revokeSlideBlobUrls(slideList: any[]) {
2572
2592
  if (!slideList || !Array.isArray(slideList)) return
2573
2593
  for (const slide of slideList) {
@@ -2585,11 +2605,7 @@ function revokeSlideBlobUrls(slideList: any[]) {
2585
2605
  }
2586
2606
  if (slide.elements && Array.isArray(slide.elements)) {
2587
2607
  for (const el of slide.elements) {
2588
- if (!el) continue
2589
- const src = el.src || el.content
2590
- if (typeof src === 'string' && src.startsWith('blob:')) {
2591
- try { URL.revokeObjectURL(src) } catch (e) {}
2592
- }
2608
+ revokeElementBlobUrls(el)
2593
2609
  }
2594
2610
  }
2595
2611
  }
@@ -2598,6 +2614,7 @@ function revokeSlideBlobUrls(slideList: any[]) {
2598
2614
  function initHistory() {
2599
2615
  ensurePageWatchers()
2600
2616
  ensureMasterWatchers()
2617
+ slidesRevision.value++ // 导入/重置整册替换数组,补一次 bump 供对外同步
2601
2618
  // 双栈即将整体重置,此时清资源池是安全的(不会有存活条目引用池内 id)
2602
2619
  assetPool.clear()
2603
2620
  assetIntern.clear()
@@ -2665,6 +2682,11 @@ function syncDOMWithRestoredSlides() {
2665
2682
  function addHistorySnapshot() {
2666
2683
  if (isUndoingRedoing.value || pauseHistory.value) return
2667
2684
  syncActiveEditingTextToElement()
2685
+ // 连接线吸附是 flush:'post' 的深度 watcher,在手动快照之后才执行:若快照先落盘,
2686
+ // 吸附结果会被防抖 watcher 再推一条幽灵条目(同一操作两条历史,撤销第一步看似无效;
2687
+ // 还原旧条目还会让吸附反复重推导,undo 前的 flush 不断推新条目,撤销指针原地打转)。
2688
+ // 序列化前先就地收敛一次(拖拽 stopDrag 里显式调 syncSlideConnections 同理),幂等无副作用。
2689
+ settleActiveSlideConnections()
2668
2690
  ensurePageWatchers()
2669
2691
 
2670
2692
  const __perfT0 = performance.now()
@@ -2715,6 +2737,9 @@ function addHistorySnapshot() {
2715
2737
 
2716
2738
  historyIndex.value = historyStack.value.length - 1
2717
2739
  currentPageIds = slides.value.map(s => s && s.id)
2740
+ // 真实落盘的编辑统一失效缩略图/画布快照:非拖拽类编辑(换形状/样式/文字提交/对齐/
2741
+ // keypoint 调整等)原先没有任何 bump 点,SidebarLeft 激活页缩略图的 v-memo 会停在旧内容
2742
+ activeSlideVersion.value++
2718
2743
  if (import.meta.env.DEV) {
2719
2744
  console.debug(`[perf:addSnapshot] ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length} dirty=${candidates.size} stack=${historyStack.value.length}`)
2720
2745
  // 漏标脏页保险丝:未纳入候选集的页应与落盘字符串完全一致,不一致说明脏标漏页(仅 DEV)
@@ -2794,6 +2819,10 @@ function scheduleHistorySnapshot() {
2794
2819
 
2795
2820
  // 每页一个 deep watcher:任何页的任何深层变更只标脏自己那一页(替代原整册 deep watch)。
2796
2821
  // 标脏不受守卫限制(守卫只作用于 flush),pauseHistory/撤销期间产生的标记靠下次 flush 的字符串比对自愈
2822
+ // 对外变更版本号:PptEditor 侧的对外同步 watch 用它替代整册 deep watch(后者每次拖拽/打字
2823
+ // 都要 O(整册) 深遍历重收集依赖,页数越多越卡)。任何变更点 bump 一次即可
2824
+ const slidesRevision = ref(0)
2825
+
2797
2826
  function ensurePageWatchers() {
2798
2827
  const n = slides.value.length
2799
2828
  while (pageWatchers.length > n) pageWatchers.pop()!()
@@ -2804,6 +2833,7 @@ function ensurePageWatchers() {
2804
2833
  // 此窗口内还原路径已同步 currentPageStrs,无需标脏。watcher 回调先于 nextTick 复位执行,守卫可靠
2805
2834
  if (isUndoingRedoing.value) return
2806
2835
  dirtyPageIndexes.add(i)
2836
+ slidesRevision.value++
2807
2837
  scheduleHistorySnapshot()
2808
2838
  }, { deep: true }))
2809
2839
  }
@@ -2812,6 +2842,7 @@ function ensurePageWatchers() {
2812
2842
  // 长度哨兵:纯尾部追加(末页后 addSlide)不触发任何 index getter,必须靠它补注册 watcher
2813
2843
  watch(() => slides.value.length, () => {
2814
2844
  ensurePageWatchers()
2845
+ slidesRevision.value++
2815
2846
  scheduleHistorySnapshot()
2816
2847
  })
2817
2848
 
@@ -2853,6 +2884,7 @@ function ensureMasterWatchers() {
2853
2884
  masterWatchers.push(watch(() => slideMasters.value[i], () => {
2854
2885
  if (isUndoingRedoing.value) return
2855
2886
  dirtyMasterIndexes.add(i)
2887
+ slidesRevision.value++
2856
2888
  scheduleMasterHistorySnapshot()
2857
2889
  }, { deep: true }))
2858
2890
  }
@@ -2860,6 +2892,7 @@ function ensureMasterWatchers() {
2860
2892
 
2861
2893
  watch(() => slideMasters.value.length, () => {
2862
2894
  ensureMasterWatchers()
2895
+ slidesRevision.value++
2863
2896
  scheduleMasterHistorySnapshot()
2864
2897
  })
2865
2898
 
@@ -2910,6 +2943,7 @@ const canRedo = computed(() => isMasterEditingView.value
2910
2943
  // 按页还原:只解析与当前状态字符串不同的页,未变页对象引用复用(缩略图/组件 vnode 稳定)。
2911
2944
  // 始终返回新数组:依赖 slides.value 数组 identity 的 watcher(缩略图测量/滚动重置等)照常触发
2912
2945
  function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list: T[]): { list: T[]; changed: boolean } {
2946
+ slidesRevision.value++ // undo/redo 窗口期页 watcher 被守卫跳过,还原本身补一次 bump 供对外同步
2913
2947
  if (entryStrs.length !== list.length || entryStrs.length !== currentStrs.length) {
2914
2948
  // 结构性差异(增删页):与旧行为一致,整册逐页解析
2915
2949
  return { list: entryStrs.map(s => parsePage<T>(s)), changed: true }
@@ -3246,47 +3280,288 @@ function pasteSlideData(targetSlideData: any, afterIndex = activeIndex.value, is
3246
3280
  }
3247
3281
  }
3248
3282
 
3249
- function pasteElementsData(elementsArray: any[]) {
3250
- 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
3251
3449
  const pastedIds: string[] = []
3450
+ const oldIdToNewId = new Map<string, string>()
3252
3451
  const oldGroupIdToNewGroupId = new Map<string, string>()
3253
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
+
3254
3520
  elementsArray.forEach((el: any) => {
3255
3521
  const newEl = JSON.parse(JSON.stringify(el))
3256
- newEl.id = (el.id && el.id.startsWith('title') ? 'title-' : '') + Date.now().toString() + Math.random().toString(36).substring(2, 7)
3257
- if (newEl.groupId) {
3258
- if (!oldGroupIdToNewGroupId.has(newEl.groupId)) {
3259
- oldGroupIdToNewGroupId.set(newEl.groupId, 'group_' + Date.now().toString() + Math.random().toString(36).substring(2, 7))
3260
- }
3261
- newEl.groupId = oldGroupIdToNewGroupId.get(newEl.groupId)
3522
+ reassignIdsRecursively(newEl)
3523
+ remapLineConnections(newEl)
3524
+
3525
+ if (needScale) {
3526
+ scaleElementIfNeeded(newEl, scaleX, scaleY)
3262
3527
  }
3263
3528
 
3264
3529
  if (newEl.style && newEl.style.left) {
3265
3530
  const leftVal = parseFloat(newEl.style.left) || 0
3266
- const isPx = newEl.style.left.endsWith('px')
3267
3531
  const isPct = newEl.style.left.endsWith('%')
3268
- if (isPx) {
3269
- newEl.style.left = `${leftVal + 20}px`
3270
- } else if (isPct) {
3271
- newEl.style.left = `${(leftVal + (20 / (logicalWidth.value || 1280)) * 100).toFixed(2)}%`
3532
+ if (isPct) {
3533
+ newEl.style.left = `${(leftVal + (offsetPx / destW) * 100).toFixed(4)}%`
3272
3534
  } else {
3273
- newEl.style.left = `${leftVal + 20}px`
3535
+ newEl.style.left = `${(leftVal + offsetPx).toFixed(1)}px`
3274
3536
  }
3275
3537
  }
3276
3538
  if (newEl.style && newEl.style.top) {
3277
3539
  const topVal = parseFloat(newEl.style.top) || 0
3278
- const isPx = newEl.style.top.endsWith('px')
3279
3540
  const isPct = newEl.style.top.endsWith('%')
3280
- if (isPx) {
3281
- newEl.style.top = `${topVal + 20}px`
3282
- } else if (isPct) {
3283
- newEl.style.top = `${(topVal + (20 / (logicalHeight.value || 720)) * 100).toFixed(2)}%`
3541
+ if (isPct) {
3542
+ newEl.style.top = `${(topVal + (offsetPx / destH) * 100).toFixed(4)}%`
3284
3543
  } else {
3285
- newEl.style.top = `${topVal + 20}px`
3544
+ newEl.style.top = `${(topVal + offsetPx).toFixed(1)}px`
3286
3545
  }
3287
3546
  }
3288
3547
 
3289
- 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)
3290
3565
  pastedIds.push(newEl.id)
3291
3566
  })
3292
3567
 
@@ -3294,6 +3569,7 @@ function pasteElementsData(elementsArray: any[]) {
3294
3569
  if (pastedIds.length > 0) {
3295
3570
  selectedElementId.value = pastedIds[pastedIds.length - 1]
3296
3571
  }
3572
+ addHistorySnapshot()
3297
3573
  notify(`已粘贴 ${pastedIds.length} 个元素`)
3298
3574
  }
3299
3575
 
@@ -3403,6 +3679,10 @@ function duplicateSelectedElements(targetSlide = activeSlide.value) {
3403
3679
  targetSlide.elements.push(cloned)
3404
3680
  }
3405
3681
 
3682
+ if (cloned.type === 'group') {
3683
+ updateGroupBounds(cloned)
3684
+ }
3685
+
3406
3686
  newElementIds.push(cloned.id)
3407
3687
  clonedElements.push(cloned)
3408
3688
  })
@@ -3483,6 +3763,12 @@ async function handleUnifiedPaste(afterSlideIndex?: number) {
3483
3763
  pasteSlideData(data.data, afterSlideIndex !== undefined ? afterSlideIndex : activeIndex.value)
3484
3764
  return true
3485
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
+ }
3486
3772
  if (Array.isArray(data) && data.length > 0 && data[0].type && data[0].style) {
3487
3773
  clipboard.value = data
3488
3774
  slideClipboard.value = null
@@ -4201,36 +4487,20 @@ function combineElements() {
4201
4487
  const idx = activeSlide.value!.elements.indexOf(el)
4202
4488
  if (idx > maxZIndex) maxZIndex = idx
4203
4489
 
4204
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4205
- const left = parseValToPx(el.style?.left, logicalWidth.value, 0)
4206
- const top = parseValToPx(el.style?.top, logicalHeight.value, 0)
4207
- let width = parseValToPx(el.style?.width, logicalWidth.value, 200)
4208
- let height = parseValToPx(el.style?.height, logicalHeight.value, 50)
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)
4209
4494
 
4210
- if (domEl) {
4211
- width = domEl.offsetWidth || width
4212
- height = domEl.offsetHeight || height
4213
- }
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)
4214
4499
 
4215
- if (width <= 0) width = 100
4216
- if (height <= 0) height = 30
4217
-
4218
- if (left < minLeft) minLeft = left
4219
- if (top < minTop) minTop = top
4220
- if (left + width > maxRight) maxRight = left + width
4221
- if (top + height > maxBottom) maxBottom = top + height
4222
- })
4223
-
4224
- targetElements.forEach(el => {
4225
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4226
- const left = parseValToPx(el.style?.left, logicalWidth.value, 0)
4227
- const top = parseValToPx(el.style?.top, logicalHeight.value, 0)
4228
- let width = parseValToPx(el.style?.width, logicalWidth.value, 200)
4229
- let height = parseValToPx(el.style?.height, logicalHeight.value, 50)
4230
-
4231
- if (domEl) {
4232
- width = domEl.offsetWidth || width
4233
- 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
4234
4504
  }
4235
4505
 
4236
4506
  if (width <= 0) width = 100
@@ -4248,15 +4518,21 @@ function combineElements() {
4248
4518
  // Convert each target element's coordinates relative to the group container (%)
4249
4519
  const childElements: SlideElement[] = targetElements.map(el => {
4250
4520
  const childClone = JSON.parse(JSON.stringify(el)) as SlideElement
4251
- const domEl = document.getElementById(`canvas-el-${el.id}`)
4252
- const elLeft = parseValToPx(el.style?.left, logicalWidth.value, 0)
4253
- const elTop = parseValToPx(el.style?.top, logicalHeight.value, 0)
4254
- let elWidth = parseValToPx(el.style?.width, logicalWidth.value, 200)
4255
- let elHeight = parseValToPx(el.style?.height, logicalHeight.value, 50)
4256
4521
 
4257
- if (domEl) {
4258
- elWidth = domEl.offsetWidth || elWidth
4259
- 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
4260
4536
  }
4261
4537
 
4262
4538
  const relLeft = ((elLeft - minLeft) / groupWidth) * 100
@@ -4270,6 +4546,20 @@ function combineElements() {
4270
4546
  childClone.style.width = `${relWidth.toFixed(4)}%`
4271
4547
  childClone.style.height = `${relHeight.toFixed(4)}%`
4272
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
+
4273
4563
  return childClone
4274
4564
  })
4275
4565
 
@@ -4287,6 +4577,10 @@ function combineElements() {
4287
4577
  height: `${((groupHeight / logicalHeight.value) * 100).toFixed(4)}%`,
4288
4578
  rotate: 0
4289
4579
  },
4580
+ left: minLeft,
4581
+ top: minTop,
4582
+ width: groupWidth,
4583
+ height: groupHeight,
4290
4584
  elements: childElements
4291
4585
  }
4292
4586
 
@@ -4359,6 +4653,22 @@ function uncombineElement(groupId?: string) {
4359
4653
  childClone.style.width = `${((absWidthPx / logicalWidth.value) * 100).toFixed(4)}%`
4360
4654
  childClone.style.height = `${((absHeightPx / logicalHeight.value) * 100).toFixed(4)}%`
4361
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
+
4362
4672
  return childClone
4363
4673
  })
4364
4674
 
@@ -4480,10 +4790,10 @@ function updateGroupBounds(groupEl: SlideElement) {
4480
4790
  groupEl.style.width = isWidthPx ? `${newGroupWidth.toFixed(1)}px` : `${((newGroupWidth / parentDim.width) * 100).toFixed(4)}%`
4481
4791
  groupEl.style.height = isHeightPx ? `${newGroupHeight.toFixed(1)}px` : `${((newGroupHeight / parentDim.height) * 100).toFixed(4)}%`
4482
4792
 
4483
- if (typeof (groupEl as any).left === 'number') (groupEl as any).left = minAbsLeft
4484
- if (typeof (groupEl as any).top === 'number') (groupEl as any).top = minAbsTop
4485
- if (typeof (groupEl as any).width === 'number') (groupEl as any).width = newGroupWidth
4486
- 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
4487
4797
 
4488
4798
  absChildren.forEach(({ child, absLeft, absTop, absWidth, absHeight }) => {
4489
4799
  const newRelLeftPct = ((absLeft - minAbsLeft) / newGroupWidth) * 100
@@ -4497,10 +4807,10 @@ function updateGroupBounds(groupEl: SlideElement) {
4497
4807
  child.style.width = `${newRelWidthPct.toFixed(4)}%`
4498
4808
  child.style.height = `${newRelHeightPct.toFixed(4)}%`
4499
4809
 
4500
- if (typeof (child as any).left === 'number') (child as any).left = absLeft - minAbsLeft
4501
- if (typeof (child as any).top === 'number') (child as any).top = absTop - minAbsTop
4502
- if (typeof (child as any).width === 'number') (child as any).width = absWidth
4503
- 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
4504
4814
  })
4505
4815
 
4506
4816
  // Recursive parent group bounds update if nested in another group
@@ -4540,6 +4850,8 @@ function deleteElement(id: string, silent = false) {
4540
4850
  const currentAnimationIndex = ref(0)
4541
4851
  const isPlayingAnimation = ref(false)
4542
4852
  const playedTriggerAnimations = ref<string[]>([])
4853
+ // 动画代际:快速跳步/换页/退出放映时 +1,使挂起的 autoNext setTimeout 链失效(防止旧链在新页继续跑)
4854
+ const animationGeneration = ref(0)
4543
4855
 
4544
4856
  function getFormatedAnimationsForSlide(slide: Slide) {
4545
4857
  if (!slide.animations) return []
@@ -4635,7 +4947,9 @@ function runAnimationStep() {
4635
4947
  if (finishedCount === animations.length) {
4636
4948
  isPlayingAnimation.value = false
4637
4949
  if (autoNext) {
4950
+ const gen = animationGeneration.value
4638
4951
  setTimeout(() => {
4952
+ if (gen !== animationGeneration.value) return
4639
4953
  runAnimationStep()
4640
4954
  }, 50)
4641
4955
  }
@@ -4661,36 +4975,6 @@ function runAnimationStep() {
4661
4975
  }
4662
4976
  }
4663
4977
 
4664
- function revokeAnimationStep() {
4665
- if (currentAnimationIndex.value <= 0) return
4666
-
4667
- currentAnimationIndex.value -= 1
4668
- const formatted = formatedAnimations.value
4669
- const { animations } = formatted[currentAnimationIndex.value]
4670
-
4671
- for (const animation of animations) {
4672
- const el = document.getElementById(`screen-element-${animation.elId}`)
4673
- if (!el) continue
4674
-
4675
- el.style.animationDuration = ''
4676
- el.style.animationDelay = ''
4677
- el.style.animationIterationCount = ''
4678
- el.style.removeProperty('--animate-duration')
4679
- el.style.removeProperty('--animate-repeat')
4680
- el.style.removeProperty('visibility')
4681
- for (const cls of Array.from(el.classList)) {
4682
- if (cls.startsWith('animate__')) {
4683
- el.classList.remove(cls)
4684
- }
4685
- }
4686
- el.classList.remove('animate__animated')
4687
- }
4688
-
4689
- if (animations.every(item => item.type === 'attention')) {
4690
- revokeAnimationStep()
4691
- }
4692
- }
4693
-
4694
4978
  function runTriggerAnimations(clickedElId: string) {
4695
4979
  if (!activeSlide.value || !activeSlide.value.animations) return
4696
4980
  const elIds = activeSlide.value.elements.map(el => el.id)
@@ -4760,11 +5044,62 @@ function restoreAnimationState(slide: Slide, targetIndex: number) {
4760
5044
  }
4761
5045
  }
4762
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
+
4763
5100
  function execNext() {
4764
5101
  const formatted = formatedAnimations.value
4765
- if (formatted.length && currentAnimationIndex.value < formatted.length) {
4766
- runAnimationStep()
4767
- } else {
5102
+ const goNextSlide = () => {
4768
5103
  if (activeIndex.value < slides.value.length - 1) {
4769
5104
  activeIndex.value++
4770
5105
  currentAnimationIndex.value = 0
@@ -4773,22 +5108,42 @@ function execNext() {
4773
5108
  notify('演示已结束')
4774
5109
  }
4775
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
+ }
4776
5126
  }
4777
5127
 
4778
5128
  function execPrev() {
4779
- if (currentAnimationIndex.value > 0) {
4780
- revokeAnimationStep()
4781
- } else {
4782
- if (activeIndex.value > 0) {
4783
- activeIndex.value--
4784
- const prevSlide = slides.value[activeIndex.value]
4785
- if (prevSlide) {
4786
- const formatted = getFormatedAnimationsForSlide(prevSlide)
4787
- currentAnimationIndex.value = formatted.length
4788
- nextTick(() => {
4789
- restoreAnimationState(prevSlide, currentAnimationIndex.value)
4790
- })
4791
- }
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
+ })
4792
5147
  }
4793
5148
  }
4794
5149
  }
@@ -4842,6 +5197,7 @@ function needWaitAnimation(slide: Slide, elId: string, animationIndex: number) {
4842
5197
 
4843
5198
  watch([activeIndex, isPresenting], () => {
4844
5199
  playedTriggerAnimations.value = []
5200
+ animationGeneration.value++ // 换页/进出放映:作废挂起的 autoNext 自动链
4845
5201
  currentAnimationIndex.value = 0
4846
5202
  if (!isPresenting.value) return
4847
5203
 
@@ -5695,6 +6051,10 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5695
6051
  if ((el.wrap === 'none' || el.wrap === false) && (!el.style.whiteSpace || el.style.whiteSpace === 'normal')) {
5696
6052
  el.style.whiteSpace = 'nowrap'
5697
6053
  }
6054
+ const rawLh = el.style?.lineHeight || el.lineHeight || el.text?.lineHeight
6055
+ if (rawLh && !el.style.lineHeight) {
6056
+ el.style.lineHeight = rawLh
6057
+ }
5698
6058
  if (el.shadow) {
5699
6059
  if (isEffectiveLocalShadow(el.shadow)) {
5700
6060
  if (!el.style.boxShadow || el.style.boxShadow === 'none') {
@@ -5865,7 +6225,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5865
6225
 
5866
6226
  const normType = String(shapType).toLowerCase()
5867
6227
 
5868
- for (const group of PPTX_SHAPE_LIST) {
6228
+ for (const group of shapeList()) {
5869
6229
  for (const item of group.children) {
5870
6230
  const itemPptxType = (item.pptxShapeType || '').toLowerCase()
5871
6231
  const itemFormula = (item.pathFormula || '').toLowerCase()
@@ -5886,8 +6246,8 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
5886
6246
  const matchedShapType = item.pptxShapeType || shapType
5887
6247
  const matchedPathFormula = item.pathFormula
5888
6248
 
5889
- if (item.pathFormula && PPTX_SHAPE_PATH_FORMULAS[item.pathFormula]) {
5890
- const formula = PPTX_SHAPE_PATH_FORMULAS[item.pathFormula]
6249
+ if (item.pathFormula && shapeFormulas()[item.pathFormula]) {
6250
+ const formula = shapeFormulas()[item.pathFormula]
5891
6251
  let kps = formula.defaultValue ? [...formula.defaultValue] : []
5892
6252
  if (el.keypoints) {
5893
6253
  if (Array.isArray(el.keypoints) && el.keypoints.length > 0) {
@@ -6640,7 +7000,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6640
7000
  let pFormula = el.pathFormula
6641
7001
  if (!pFormula && el.shapType) {
6642
7002
  const norm = String(el.shapType).toLowerCase()
6643
- 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) => {
6644
7004
  const t1 = (item.pptxShapeType || '').toLowerCase()
6645
7005
  const t2 = (item.pathFormula || '').toLowerCase()
6646
7006
  return t1 === norm || t2 === norm ||
@@ -6655,7 +7015,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6655
7015
  if (shape) pFormula = shape.pathFormula
6656
7016
  }
6657
7017
  if (pFormula) {
6658
- const formulaObj = PPTX_SHAPE_PATH_FORMULAS[pFormula as string]
7018
+ const formulaObj = shapeFormulas()[pFormula as string]
6659
7019
  if (formulaObj && formulaObj.defaultValue) {
6660
7020
  const kps = el.keypoints && Array.isArray(el.keypoints) && el.keypoints.length > 0 ? [...el.keypoints] : []
6661
7021
  const defaults = formulaObj.defaultValue
@@ -6696,7 +7056,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6696
7056
  pathFormula: (() => {
6697
7057
  if (type === 'shape' && el.shapType && !el.pathFormula) {
6698
7058
  const norm = String(el.shapType).toLowerCase()
6699
- 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) => {
6700
7060
  const t1 = (item.pptxShapeType || '').toLowerCase()
6701
7061
  const t2 = (item.pathFormula || '').toLowerCase()
6702
7062
  return t1 === norm || t2 === norm ||
@@ -7845,6 +8205,7 @@ async function aiReplaceCurrentSlide(options?: { primaryCategory?: string; secon
7845
8205
  notify('正在进行一键美化,替换当前幻灯片内容...')
7846
8206
 
7847
8207
  try {
8208
+ await ensurePptxShapes() // 后续 pptParserFn/convertPPTistToLocalIfNeed 同步消费形状表,先确保就绪
7848
8209
  const pureText = extractPureTextFromSlide(activeSlide.value)
7849
8210
  if (!pureText) {
7850
8211
  notify('当前幻灯片没有可提取的纯文字内容')
@@ -8883,6 +9244,7 @@ export function useSlides() {
8883
9244
  zoomRatio,
8884
9245
  fitTrigger,
8885
9246
  slides,
9247
+ slidesRevision,
8886
9248
  activeIndex,
8887
9249
  activeSlide,
8888
9250
  selectedSlideIndexes,
@@ -8963,6 +9325,7 @@ export function useSlides() {
8963
9325
  slideClipboard,
8964
9326
  pasteSlideData,
8965
9327
  pasteElementsData,
9328
+ copySelectedElements,
8966
9329
  handleUnifiedPaste,
8967
9330
  shapeFormatPainter,
8968
9331
  currentSlideAnimations,