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.
- package/README.md +60 -2
- package/dist/lib/jvs-ppt-ui.es.js +22404 -21454
- package/dist/lib/jvs-ppt-ui.umd.js +102 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +53 -53
- package/src/components/AIPptDialog.vue +2 -0
- package/src/components/CanvasArea.vue +164 -72
- package/src/components/Header.vue +374 -144
- package/src/components/PptEditor.vue +33 -15
- package/src/components/PresentationMode.vue +24 -4
- package/src/components/SidebarRight.vue +8 -1
- package/src/components/SvgChart.vue +5 -2
- package/src/components/TemplateGallery.vue +1321 -866
- package/src/components/Toolbar.vue +244 -22
- package/src/components/canvas/CanvasTransformHandles.vue +5 -1
- package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
- package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
- package/src/components/canvas/textSelectionOverlay.ts +305 -0
- package/src/composables/canvas/useCanvasAlignment.ts +286 -238
- package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
- package/src/composables/canvas/useCanvasFormatting.ts +35 -12
- package/src/composables/canvas/useCanvasTable.ts +52 -24
- package/src/composables/canvas/useCanvasText.ts +12 -0
- package/src/composables/canvas/useCanvasTransform.ts +15 -2
- package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
- package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
- package/src/composables/useSlides.ts +495 -132
- package/src/style.css +88 -0
- package/src/types/index.ts +8 -0
- package/src/utils/line.ts +274 -312
- package/src/utils/pptxShapes.ts +1 -1
- package/src/utils/pptxShapesLazy.ts +39 -0
- 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 {
|
|
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
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
3250
|
-
if (!activeSlide.value || !
|
|
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
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
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 (
|
|
3269
|
-
newEl.style.left = `${leftVal +
|
|
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 +
|
|
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 (
|
|
3281
|
-
newEl.style.top = `${topVal +
|
|
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 +
|
|
3544
|
+
newEl.style.top = `${(topVal + offsetPx).toFixed(1)}px`
|
|
3286
3545
|
}
|
|
3287
3546
|
}
|
|
3288
3547
|
|
|
3289
|
-
|
|
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
|
|
4205
|
-
const
|
|
4206
|
-
const
|
|
4207
|
-
|
|
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
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
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
|
-
|
|
4216
|
-
if (
|
|
4217
|
-
|
|
4218
|
-
|
|
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
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
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
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
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
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
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
|
|
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 &&
|
|
5890
|
-
const formula =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|