jvs-ppt-ui 0.0.7 → 0.0.9

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.
@@ -1,4 +1,4 @@
1
- import { ref, computed, watch, nextTick } from 'vue'
1
+ 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'
@@ -1737,6 +1737,11 @@ function applyStyleRecipeToMaster(
1737
1737
  globalBgColor.value = recipe.backgroundColor
1738
1738
  }
1739
1739
 
1740
+ // 同 tick 多母版变更 + 急切快照:pre-flush watcher 尚未排空,显式标脏目标母版
1741
+ targetMasters.forEach(m => {
1742
+ const i = slideMasters.value.indexOf(m)
1743
+ if (i >= 0) dirtyMasterIndexes.add(i)
1744
+ })
1740
1745
  addMasterHistorySnapshot()
1741
1746
  notify(isAll ? `已应用主题「${recipe.name}」到全部母版` : `已应用主题「${recipe.name}」到当前母版`)
1742
1747
  }
@@ -1848,6 +1853,8 @@ function applyStyleRecipeToSlides(
1848
1853
  globalThemeColor.value = [...recipe.styleColors]
1849
1854
  }
1850
1855
 
1856
+ // 同 tick 多页变更 + 急切快照:非 single 模式涉及多页,显式全量标脏(字符串比对会自动去重)
1857
+ if (effectiveMode !== 'single') markAllPagesDirty()
1851
1858
  addHistorySnapshot()
1852
1859
  if (effectiveMode === 'all') {
1853
1860
  notify(`已应用主题「${recipe.name}」到全部幻灯片与母版`)
@@ -2495,20 +2502,84 @@ function notify(msg: string) {
2495
2502
  }, 2500)
2496
2503
  }
2497
2504
 
2498
- // 历史栈只存 slides 的 JSON 字符串;每条快照对应的激活页记录在平行数组中,
2499
- // 翻页时只更新一个数字,不再对整册快照做 parse + stringify
2500
- const historyStack = ref<string[]>([])
2505
+ // 历史栈按页存 JSON 字符串:每条快照是 string[](每页一条),未变页的字符串引用跨快照共享,
2506
+ // 编辑只序列化脏页、撤销只解析变更页;每条快照对应的激活页仍记在平行数组中,翻页只改一个数字
2507
+ const historyStack = ref<string[][]>([])
2501
2508
  const historyIndex = ref(-1)
2502
2509
  const isUndoingRedoing = ref(false)
2503
2510
  let historyActiveIndexes: number[] = []
2504
2511
 
2505
- const masterHistoryStack = ref<string[]>([])
2512
+ const masterHistoryStack = ref<string[][]>([])
2506
2513
  const masterHistoryIndex = ref(-1)
2507
- let lastSnapshotMastersStr = ''
2508
2514
  // 母版栈同理:激活的母版/版式 id 记在平行数组,切换母版不触发整册重序列化
2509
2515
  let masterHistoryActiveMids: string[] = []
2510
2516
  let masterHistoryActiveLids: string[] = []
2511
2517
 
2518
+ // --- 按页脏跟踪(slides 与 slideMasters 各一套) ---
2519
+ let pageWatchers: WatchStopHandle[] = []
2520
+ const dirtyPageIndexes = new Set<number>()
2521
+ let currentPageStrs: string[] = [] // 最近一次落盘的每页字符串(与栈内条目按引用共享)
2522
+ let currentPageIds: (string | undefined)[] = [] // 最近一次落盘的每页 id,用于重排漏检兜底
2523
+
2524
+ let masterWatchers: WatchStopHandle[] = []
2525
+ const dirtyMasterIndexes = new Set<number>()
2526
+ let currentMasterStrs: string[] = []
2527
+ let currentMasterIds: (string | undefined)[] = []
2528
+
2529
+ // --- 资源池:data URL 大字符串 intern 成短 id,避免随每条快照重复拷贝 ---
2530
+ // 池只在 initHistory 清空(届时双栈同时重置,不会留下悬空 id);slides 与 masters 共用
2531
+ const assetPool = new Map<string, string>() // id -> 原始 data URL
2532
+ const assetIntern = new Map<string, string>() // 原始 data URL -> id
2533
+ let assetSeq = 0
2534
+
2535
+ const ASSET_MIN_LEN = 1024
2536
+
2537
+ function assetReplacer(_k: string, v: any): any {
2538
+ if (typeof v === 'string' && v.length >= ASSET_MIN_LEN && v.charCodeAt(0) === 100 && v.startsWith('data:')) {
2539
+ let id = assetIntern.get(v)
2540
+ if (id === undefined) {
2541
+ assetSeq += 1
2542
+ id = `@@JVSAsset:${assetSeq}@@`
2543
+ assetIntern.set(v, id)
2544
+ assetPool.set(id, v)
2545
+ }
2546
+ return id
2547
+ }
2548
+ return v
2549
+ }
2550
+
2551
+ function assetReviver(_k: string, v: any): any {
2552
+ if (typeof v === 'string' && v.length <= 32 && v.charCodeAt(0) === 64) {
2553
+ const hit = assetPool.get(v)
2554
+ if (hit !== undefined) return hit
2555
+ }
2556
+ return v
2557
+ }
2558
+
2559
+ function serializePage(page: any): string {
2560
+ return JSON.stringify(page, assetReplacer)
2561
+ }
2562
+
2563
+ function parsePage<T>(s: string): T {
2564
+ return JSON.parse(s, assetReviver) as T
2565
+ }
2566
+
2567
+ function markAllPagesDirty() {
2568
+ for (let i = 0; i < slides.value.length; i++) dirtyPageIndexes.add(i)
2569
+ }
2570
+
2571
+ // 递归释放元素(含组合内子元素)的 blob: URL,防止导入替换后组内媒体泄漏
2572
+ function revokeElementBlobUrls(el: any) {
2573
+ if (!el) return
2574
+ if (Array.isArray(el.elements)) {
2575
+ for (const child of el.elements) revokeElementBlobUrls(child)
2576
+ }
2577
+ const src = el.src || el.content
2578
+ if (typeof src === 'string' && src.startsWith('blob:')) {
2579
+ try { URL.revokeObjectURL(src) } catch (e) {}
2580
+ }
2581
+ }
2582
+
2512
2583
  function revokeSlideBlobUrls(slideList: any[]) {
2513
2584
  if (!slideList || !Array.isArray(slideList)) return
2514
2585
  for (const slide of slideList) {
@@ -2526,35 +2597,40 @@ function revokeSlideBlobUrls(slideList: any[]) {
2526
2597
  }
2527
2598
  if (slide.elements && Array.isArray(slide.elements)) {
2528
2599
  for (const el of slide.elements) {
2529
- if (!el) continue
2530
- const src = el.src || el.content
2531
- if (typeof src === 'string' && src.startsWith('blob:')) {
2532
- try { URL.revokeObjectURL(src) } catch (e) {}
2533
- }
2600
+ revokeElementBlobUrls(el)
2534
2601
  }
2535
2602
  }
2536
2603
  }
2537
2604
  }
2538
2605
 
2539
2606
  function initHistory() {
2540
- const slidesStr = JSON.stringify(slides.value)
2541
- historyStack.value = [slidesStr]
2607
+ ensurePageWatchers()
2608
+ ensureMasterWatchers()
2609
+ // 双栈即将整体重置,此时清资源池是安全的(不会有存活条目引用池内 id)
2610
+ assetPool.clear()
2611
+ assetIntern.clear()
2612
+ assetSeq = 0
2613
+ dirtyPageIndexes.clear()
2614
+ dirtyMasterIndexes.clear()
2615
+
2616
+ if (import.meta.env.DEV) console.debug(`[perf:initHistory] pages=${slides.value.length} masters=${slideMasters.value.length}`)
2617
+
2618
+ currentPageStrs = slides.value.map(serializePage)
2619
+ currentPageIds = slides.value.map(s => s && s.id)
2620
+ historyStack.value = [currentPageStrs.slice()]
2542
2621
  historyActiveIndexes = [activeIndex.value]
2543
2622
  historyIndex.value = 0
2544
- lastSnapshotSlidesStr = slidesStr
2545
2623
 
2546
2624
  // 母版历史独立初始化(导入/重置演示文稿时同样清空)
2547
- const mastersStr = JSON.stringify(slideMasters.value)
2548
- masterHistoryStack.value = [mastersStr]
2625
+ currentMasterStrs = slideMasters.value.map(serializePage)
2626
+ currentMasterIds = slideMasters.value.map(m => m && m.id)
2627
+ masterHistoryStack.value = [currentMasterStrs.slice()]
2549
2628
  masterHistoryActiveMids = [activeMasterId.value || '']
2550
2629
  masterHistoryActiveLids = [activeLayoutId.value || '']
2551
2630
  masterHistoryIndex.value = 0
2552
- lastSnapshotMastersStr = mastersStr
2553
2631
  activeSlideVersion.value++
2554
2632
  }
2555
2633
 
2556
- let lastSnapshotSlidesStr = ''
2557
-
2558
2634
  function syncActiveEditingTextToElement() {
2559
2635
  if (typeof document === 'undefined' || !slides.value) return
2560
2636
  const curSlide = slides.value[activeIndex.value]
@@ -2565,8 +2641,11 @@ function syncActiveEditingTextToElement() {
2565
2641
  if (container && (!el.content || !el.content.includes('<table'))) {
2566
2642
  const htmlVal = container.innerHTML
2567
2643
  if (el.type === 'shape') {
2568
- el.text = htmlVal
2569
- } else {
2644
+ // 空 DOM 抓到 '' 时不能给无文本 shape 制造 text:"" 新键:
2645
+ // 语义等价但字符串不等,会被当作变更推入幽灵快照,导致 undo 永远撤不到底
2646
+ const isEmptyNoop = htmlVal === '' && !el.text
2647
+ if (!isEmptyNoop && el.text !== htmlVal) el.text = htmlVal
2648
+ } else if (el.content !== htmlVal) {
2570
2649
  el.content = htmlVal
2571
2650
  }
2572
2651
  }
@@ -2594,19 +2673,47 @@ function syncDOMWithRestoredSlides() {
2594
2673
  function addHistorySnapshot() {
2595
2674
  if (isUndoingRedoing.value || pauseHistory.value) return
2596
2675
  syncActiveEditingTextToElement()
2597
- const currentSlidesStr = JSON.stringify(slides.value)
2598
- if (currentSlidesStr === lastSnapshotSlidesStr) {
2676
+ ensurePageWatchers()
2677
+
2678
+ const __perfT0 = performance.now()
2679
+ const n = slides.value.length
2680
+ // 结构性变化(增删页/watcher 数漂移)时全量比对;否则只比对候选脏页
2681
+ const structural = n !== currentPageStrs.length || pageWatchers.length !== n
2682
+ const candidates: Set<number> = new Set(structural ? undefined : dirtyPageIndexes)
2683
+ if (!structural) {
2684
+ // 同 tick 内先改数据再急切调用 addHistorySnapshot 的调用点全部只动当前页,标当前页兜底
2685
+ candidates.add(activeIndex.value)
2686
+ // id 错位说明发生过 watcher 覆盖不到的重排/对调,把这些页也纳入比对
2687
+ for (let i = 0; i < n; i++) {
2688
+ if (slides.value[i]?.id !== currentPageIds[i]) candidates.add(i)
2689
+ }
2690
+ } else {
2691
+ for (let i = 0; i < n; i++) candidates.add(i)
2692
+ }
2693
+
2694
+ let changed = false
2695
+ for (const i of candidates) {
2696
+ const s = serializePage(slides.value[i])
2697
+ if (s !== currentPageStrs[i]) {
2698
+ currentPageStrs[i] = s
2699
+ changed = true
2700
+ }
2701
+ }
2702
+ currentPageStrs.length = n
2703
+ dirtyPageIndexes.clear()
2704
+
2705
+ if (!changed) {
2706
+ if (import.meta.env.DEV) console.debug(`[perf:addSnapshot-dedup] ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length} dirty=${candidates.size}`)
2599
2707
  return
2600
2708
  }
2601
- lastSnapshotSlidesStr = currentSlidesStr
2602
2709
 
2603
2710
  if (historyIndex.value >= 0) {
2604
2711
  historyStack.value = historyStack.value.slice(0, historyIndex.value + 1)
2605
2712
  historyActiveIndexes = historyActiveIndexes.slice(0, historyIndex.value + 1)
2606
2713
  }
2607
2714
 
2608
- // 零拷贝优化:直接合成快照字符串,避免反序列化为海量对象后再重新序列化的巨大内存与CPU开销
2609
- historyStack.value.push(currentSlidesStr)
2715
+ // 未变页的字符串引用跨快照共享(string 不可变,零拷贝)
2716
+ historyStack.value.push(currentPageStrs.slice())
2610
2717
  historyActiveIndexes.push(activeIndex.value)
2611
2718
 
2612
2719
  if (historyStack.value.length > 15) {
@@ -2615,20 +2722,48 @@ function addHistorySnapshot() {
2615
2722
  }
2616
2723
 
2617
2724
  historyIndex.value = historyStack.value.length - 1
2725
+ currentPageIds = slides.value.map(s => s && s.id)
2726
+ if (import.meta.env.DEV) {
2727
+ console.debug(`[perf:addSnapshot] ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length} dirty=${candidates.size} stack=${historyStack.value.length}`)
2728
+ // 漏标脏页保险丝:未纳入候选集的页应与落盘字符串完全一致,不一致说明脏标漏页(仅 DEV)
2729
+ const __bad = slides.value.findIndex((s: any, i: number) => !candidates.has(i) && serializePage(s) !== currentPageStrs[i])
2730
+ if (__bad >= 0) console.error(`[history-parity] 第 ${__bad} 页未被标脏但内容已变化:存在漏标脏页!`)
2731
+ }
2618
2732
  }
2619
2733
 
2620
2734
  function addMasterHistorySnapshot() {
2621
2735
  if (isUndoingRedoing.value || pauseHistory.value) return
2622
- const currentMastersStr = JSON.stringify(slideMasters.value)
2623
- if (currentMastersStr === lastSnapshotMastersStr) {
2624
- return
2736
+ ensureMasterWatchers()
2737
+
2738
+ const n = slideMasters.value.length
2739
+ const structural = n !== currentMasterStrs.length || masterWatchers.length !== n
2740
+ const candidates: Set<number> = new Set(structural ? undefined : dirtyMasterIndexes)
2741
+ if (!structural) {
2742
+ const activeIdx = slideMasters.value.findIndex(m => m.id === activeMasterId.value)
2743
+ if (activeIdx >= 0) candidates.add(activeIdx)
2744
+ for (let i = 0; i < n; i++) {
2745
+ if (slideMasters.value[i]?.id !== currentMasterIds[i]) candidates.add(i)
2746
+ }
2747
+ } else {
2748
+ for (let i = 0; i < n; i++) candidates.add(i)
2749
+ }
2750
+
2751
+ let changed = false
2752
+ for (const i of candidates) {
2753
+ const s = serializePage(slideMasters.value[i])
2754
+ if (s !== currentMasterStrs[i]) {
2755
+ currentMasterStrs[i] = s
2756
+ changed = true
2757
+ }
2625
2758
  }
2626
- lastSnapshotMastersStr = currentMastersStr
2759
+ currentMasterStrs.length = n
2760
+ dirtyMasterIndexes.clear()
2761
+ if (!changed) return
2627
2762
 
2628
2763
  masterHistoryStack.value = masterHistoryStack.value.slice(0, masterHistoryIndex.value + 1)
2629
2764
  masterHistoryActiveMids = masterHistoryActiveMids.slice(0, masterHistoryIndex.value + 1)
2630
2765
  masterHistoryActiveLids = masterHistoryActiveLids.slice(0, masterHistoryIndex.value + 1)
2631
- masterHistoryStack.value.push(currentMastersStr)
2766
+ masterHistoryStack.value.push(currentMasterStrs.slice())
2632
2767
  masterHistoryActiveMids.push(activeMasterId.value || '')
2633
2768
  masterHistoryActiveLids.push(activeLayoutId.value || '')
2634
2769
 
@@ -2639,6 +2774,7 @@ function addMasterHistorySnapshot() {
2639
2774
  }
2640
2775
 
2641
2776
  masterHistoryIndex.value = masterHistoryStack.value.length - 1
2777
+ currentMasterIds = slideMasters.value.map(m => m && m.id)
2642
2778
  }
2643
2779
 
2644
2780
  let historySnapshotTimer: any = null
@@ -2664,9 +2800,28 @@ function scheduleHistorySnapshot() {
2664
2800
  }, 350)
2665
2801
  }
2666
2802
 
2667
- watch(slides, () => {
2803
+ // 每页一个 deep watcher:任何页的任何深层变更只标脏自己那一页(替代原整册 deep watch)。
2804
+ // 标脏不受守卫限制(守卫只作用于 flush),pauseHistory/撤销期间产生的标记靠下次 flush 的字符串比对自愈
2805
+ function ensurePageWatchers() {
2806
+ const n = slides.value.length
2807
+ while (pageWatchers.length > n) pageWatchers.pop()!()
2808
+ while (pageWatchers.length < n) {
2809
+ const i = pageWatchers.length
2810
+ pageWatchers.push(watch(() => slides.value[i], () => {
2811
+ // undo/redo 整体替换数组会触发全部页 watcher(deep 回调不比较新旧值),全是假阳性;
2812
+ // 此窗口内还原路径已同步 currentPageStrs,无需标脏。watcher 回调先于 nextTick 复位执行,守卫可靠
2813
+ if (isUndoingRedoing.value) return
2814
+ dirtyPageIndexes.add(i)
2815
+ scheduleHistorySnapshot()
2816
+ }, { deep: true }))
2817
+ }
2818
+ }
2819
+
2820
+ // 长度哨兵:纯尾部追加(末页后 addSlide)不触发任何 index getter,必须靠它补注册 watcher
2821
+ watch(() => slides.value.length, () => {
2822
+ ensurePageWatchers()
2668
2823
  scheduleHistorySnapshot()
2669
- }, { deep: true })
2824
+ })
2670
2825
 
2671
2826
  watch(activeIndex, (newIdx) => {
2672
2827
  if (isUndoingRedoing.value) return
@@ -2698,9 +2853,23 @@ function scheduleMasterHistorySnapshot() {
2698
2853
  }, 350)
2699
2854
  }
2700
2855
 
2701
- watch(slideMasters, () => {
2856
+ function ensureMasterWatchers() {
2857
+ const n = slideMasters.value.length
2858
+ while (masterWatchers.length > n) masterWatchers.pop()!()
2859
+ while (masterWatchers.length < n) {
2860
+ const i = masterWatchers.length
2861
+ masterWatchers.push(watch(() => slideMasters.value[i], () => {
2862
+ if (isUndoingRedoing.value) return
2863
+ dirtyMasterIndexes.add(i)
2864
+ scheduleMasterHistorySnapshot()
2865
+ }, { deep: true }))
2866
+ }
2867
+ }
2868
+
2869
+ watch(() => slideMasters.value.length, () => {
2870
+ ensureMasterWatchers()
2702
2871
  scheduleMasterHistorySnapshot()
2703
- }, { deep: true })
2872
+ })
2704
2873
 
2705
2874
  // 切换母版/版式只更新栈顶快照的激活状态(与 activeIndex 同理),不产生新历史记录
2706
2875
  watch([activeMasterId, activeLayoutId], ([mid, lid]) => {
@@ -2746,18 +2915,39 @@ const canRedo = computed(() => isMasterEditingView.value
2746
2915
  ? masterHistoryIndex.value < masterHistoryStack.value.length - 1
2747
2916
  : historyIndex.value < historyStack.value.length - 1)
2748
2917
 
2918
+ // 按页还原:只解析与当前状态字符串不同的页,未变页对象引用复用(缩略图/组件 vnode 稳定)。
2919
+ // 始终返回新数组:依赖 slides.value 数组 identity 的 watcher(缩略图测量/滚动重置等)照常触发
2920
+ function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list: T[]): { list: T[]; changed: boolean } {
2921
+ if (entryStrs.length !== list.length || entryStrs.length !== currentStrs.length) {
2922
+ // 结构性差异(增删页):与旧行为一致,整册逐页解析
2923
+ return { list: entryStrs.map(s => parsePage<T>(s)), changed: true }
2924
+ }
2925
+ let changed = false
2926
+ const next = list.map((page, i) => {
2927
+ if (entryStrs[i] === currentStrs[i]) return page
2928
+ changed = true
2929
+ return parsePage<T>(entryStrs[i])
2930
+ })
2931
+ return { list: next, changed }
2932
+ }
2933
+
2749
2934
  function undo() {
2750
2935
  if (isMasterEditingView.value) {
2751
2936
  // 先落盘防抖中的快照:编辑后 350ms 内撤销不再丢最后一步编辑
2752
2937
  flushPendingMasterHistorySnapshot()
2753
2938
  if (masterHistoryIndex.value > 0) {
2939
+ const __perfT0 = performance.now()
2754
2940
  isUndoingRedoing.value = true
2755
2941
  masterHistoryIndex.value--
2756
- slideMasters.value = JSON.parse(masterHistoryStack.value[masterHistoryIndex.value]) as SlideMaster[]
2942
+ const entry = masterHistoryStack.value[masterHistoryIndex.value]
2943
+ slideMasters.value = applyHistoryEntry<SlideMaster>(entry, currentMasterStrs, slideMasters.value).list
2944
+ currentMasterStrs = entry.slice()
2945
+ currentMasterIds = slideMasters.value.map(m => m && m.id)
2757
2946
  activeMasterId.value = masterHistoryActiveMids[masterHistoryIndex.value]
2758
2947
  activeLayoutId.value = masterHistoryActiveLids[masterHistoryIndex.value]
2759
2948
  selectedElementId.value = null
2760
2949
  selectedElementIds.value = []
2950
+ if (import.meta.env.DEV) console.debug(`[perf:undo-master] patch ${(performance.now() - __perfT0).toFixed(1)}ms masters=${slideMasters.value.length}`)
2761
2951
  notify('已撤销')
2762
2952
  nextTick(() => {
2763
2953
  isUndoingRedoing.value = false
@@ -2770,14 +2960,19 @@ function undo() {
2770
2960
  // 先落盘防抖中的快照:编辑后 350ms 内撤销不再丢最后一步编辑
2771
2961
  flushPendingHistorySnapshot()
2772
2962
  if (canUndo.value) {
2963
+ const __perfT0 = performance.now()
2773
2964
  isUndoingRedoing.value = true
2774
2965
  historyIndex.value--
2775
- slides.value = JSON.parse(historyStack.value[historyIndex.value]) as Slide[]
2966
+ const entry = historyStack.value[historyIndex.value]
2967
+ slides.value = applyHistoryEntry<Slide>(entry, currentPageStrs, slides.value).list
2968
+ currentPageStrs = entry.slice()
2969
+ currentPageIds = slides.value.map(s => s && s.id)
2776
2970
  activeIndex.value = historyActiveIndexes[historyIndex.value]
2777
2971
  selectedElementId.value = null
2778
2972
  selectedElementIds.value = []
2779
2973
  syncDOMWithRestoredSlides()
2780
2974
  activeSlideVersion.value++
2975
+ if (import.meta.env.DEV) console.debug(`[perf:undo] patch ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length}`)
2781
2976
  notify('已撤销')
2782
2977
  nextTick(() => {
2783
2978
  isUndoingRedoing.value = false
@@ -2791,15 +2986,20 @@ function redo() {
2791
2986
  if (isMasterEditingView.value) {
2792
2987
  flushPendingMasterHistorySnapshot()
2793
2988
  if (masterHistoryIndex.value < masterHistoryStack.value.length - 1) {
2989
+ const __perfT0 = performance.now()
2794
2990
  isUndoingRedoing.value = true
2795
2991
  masterHistoryIndex.value++
2796
- slideMasters.value = JSON.parse(masterHistoryStack.value[masterHistoryIndex.value]) as SlideMaster[]
2992
+ const entry = masterHistoryStack.value[masterHistoryIndex.value]
2993
+ slideMasters.value = applyHistoryEntry<SlideMaster>(entry, currentMasterStrs, slideMasters.value).list
2994
+ currentMasterStrs = entry.slice()
2995
+ currentMasterIds = slideMasters.value.map(m => m && m.id)
2797
2996
  activeMasterId.value = masterHistoryActiveMids[masterHistoryIndex.value]
2798
2997
  activeLayoutId.value = masterHistoryActiveLids[masterHistoryIndex.value]
2799
2998
  selectedElementId.value = null
2800
2999
  selectedElementIds.value = []
2801
3000
  syncDOMWithRestoredSlides()
2802
3001
  activeSlideVersion.value++
3002
+ if (import.meta.env.DEV) console.debug(`[perf:redo-master] patch ${(performance.now() - __perfT0).toFixed(1)}ms masters=${slideMasters.value.length}`)
2803
3003
  notify('已重做')
2804
3004
  nextTick(() => {
2805
3005
  isUndoingRedoing.value = false
@@ -2811,14 +3011,19 @@ function redo() {
2811
3011
  }
2812
3012
  flushPendingHistorySnapshot()
2813
3013
  if (canRedo.value) {
3014
+ const __perfT0 = performance.now()
2814
3015
  isUndoingRedoing.value = true
2815
3016
  historyIndex.value++
2816
- slides.value = JSON.parse(historyStack.value[historyIndex.value]) as Slide[]
3017
+ const entry = historyStack.value[historyIndex.value]
3018
+ slides.value = applyHistoryEntry<Slide>(entry, currentPageStrs, slides.value).list
3019
+ currentPageStrs = entry.slice()
3020
+ currentPageIds = slides.value.map(s => s && s.id)
2817
3021
  activeIndex.value = historyActiveIndexes[historyIndex.value]
2818
3022
  selectedElementId.value = null
2819
3023
  selectedElementIds.value = []
2820
3024
  syncDOMWithRestoredSlides()
2821
3025
  activeSlideVersion.value++
3026
+ if (import.meta.env.DEV) console.debug(`[perf:redo] patch ${(performance.now() - __perfT0).toFixed(1)}ms pages=${slides.value.length}`)
2822
3027
  notify('已重做')
2823
3028
  nextTick(() => {
2824
3029
  isUndoingRedoing.value = false
package/src/index.ts CHANGED
@@ -4,7 +4,8 @@ import jvsPickerColorV3 from 'jvs-picker-color-v3'
4
4
  import PptEditor from './components/PptEditor.vue'
5
5
  import { useSlides } from './composables/useSlides'
6
6
 
7
- // 自动打入所有依赖样式,合并至 dist/lib/style.css,宿主无需额外引入 element-plus 样式
7
+ // 自动打入所有依赖样式与图标库,宿主无需在 index.html 额外引入 iconfont 或 element-plus 样式
8
+ import './assets/iconfont/iconfont.js'
8
9
  import 'element-plus/dist/index.css'
9
10
  import 'jvs-picker-color-v3/lib/jvs-picker-color-v3.css'
10
11
  import 'animate.css'
@@ -92,6 +92,8 @@ export type CustomUploadHandler = (
92
92
  export interface PptEditorConfig {
93
93
  /** 演示文稿初始标题 */
94
94
  title?: string
95
+ /** 是否显示左上角主菜单按钮(导入/导出/快捷键等弹窗菜单),默认为 true */
96
+ showMenu?: boolean
95
97
  /** 是否启用 AI PPT 智能生成与一键美化功能。未配置或为 false 时不显示一键美化功能 */
96
98
  enableAi?: boolean
97
99
  /** AI 一键美化请求接口地址。如果未配置或未开启,则不显示一键美化功能 */
@@ -138,6 +140,8 @@ export interface PptEditorProps {
138
140
  readonly?: boolean
139
141
  /** 是否显示顶部导航与工具栏 */
140
142
  showHeader?: boolean
143
+ /** 是否显示左上角主菜单按钮(导入/导出/快捷键等弹窗菜单),默认为 true */
144
+ showMenu?: boolean
141
145
  /** 是否显示左侧缩略图导航栏 */
142
146
  showSidebarLeft?: boolean
143
147
  /** 是否显示右侧属性面板 */