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.
- package/README.md +57 -2
- package/dist/lib/jvs-ppt-ui.es.js +135146 -112663
- package/dist/lib/jvs-ppt-ui.umd.js +101 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CanvasArea.vue +91 -21
- package/src/components/Header.vue +42 -23
- package/src/components/PptEditor.vue +1047 -1005
- package/src/components/PresentationMode.vue +3 -1
- package/src/components/SidebarLeft.vue +14 -7
- package/src/components/Toolbar.vue +2 -1
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +1341 -1336
- package/src/composables/canvas/useCanvasTable.ts +183 -146
- package/src/composables/canvas/useCanvasTransform.ts +13 -0
- package/src/composables/sidebar/useSidebarTextFormat.ts +1 -1
- package/src/composables/useSlides.ts +244 -39
- package/src/index.ts +2 -1
- package/src/types/index.ts +4 -0
- package/src/utils/line.ts +274 -312
- package/dist/lib/__vite-browser-external-CbV4tiB6.js +0 -7
- package/dist/lib/dist-DkuYVyNR.js +0 -16010
- package/dist/lib/es-CfdgxdYp.js +0 -477
- package/dist/lib/jszip.min-Kba9Mb0l.js +0 -3034
- package/dist/lib/pptxgen.es-D0HrgUe_.js +0 -2352
|
@@ -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
|
-
//
|
|
2499
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2541
|
-
|
|
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
|
-
|
|
2548
|
-
|
|
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
|
-
|
|
2569
|
-
|
|
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
|
-
|
|
2598
|
-
|
|
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
|
-
//
|
|
2609
|
-
historyStack.value.push(
|
|
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
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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'
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
/** 是否显示右侧属性面板 */
|