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
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import PptEditor from './components/PptEditor.vue'
|
|
4
|
-
import type { DeckData, PptEditorApi, UploadResourceContext } from './types'
|
|
5
|
-
|
|
6
|
-
const editorRef = ref<PptEditorApi | null>(null)
|
|
7
|
-
|
|
8
|
-
function handleSave(deck: DeckData) {
|
|
9
|
-
console.log('[JVS-PPT 独立运行模式] 保存幻灯片:', deck)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function handleReady(api: PptEditorApi) {
|
|
13
|
-
console.log('[JVS-PPT 独立运行模式] 编辑器初始化完成:', api)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 模拟资源上传函数
|
|
18
|
-
* 覆盖场景:导入 PPTX/JSON、上传图片、视频、音频、剪贴板粘贴、占位符上传、背景上传等
|
|
19
|
-
*/
|
|
20
|
-
async function handleCustomUpload(fileOrBlob: File | Blob, context: UploadResourceContext): Promise<string> {
|
|
21
|
-
console.log('[模拟自定义上传函数触发] 接收到待上传资源:', {
|
|
22
|
-
type: context.type, // 资源类型: 'image' | 'video' | 'audio'
|
|
23
|
-
source: context.source, // 触发来源: 'file-upload' | 'pptx-import' | 'json-import' | 'paste' | 'placeholder' | 'background'
|
|
24
|
-
filename: context.filename,
|
|
25
|
-
size: `${(fileOrBlob.size / 1024).toFixed(2)} KB`,
|
|
26
|
-
fileOrBlob
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// 1. 模拟网络上传请求耗时
|
|
30
|
-
await new Promise(resolve => setTimeout(resolve, 300))
|
|
31
|
-
|
|
32
|
-
// 2. 实际业务生产代码范例:
|
|
33
|
-
// const formData = new FormData()
|
|
34
|
-
// formData.append('file', fileOrBlob, context.filename || `upload_${Date.now()}`)
|
|
35
|
-
// formData.append('type', context.type)
|
|
36
|
-
// const res = await fetch('/api/oss/upload', { method: 'POST', body: formData }).then(r => r.json())
|
|
37
|
-
// return res.data.url
|
|
38
|
-
|
|
39
|
-
// 3. 模拟环境:生成可正常渲染的地址作为模拟的服务器返回 URL
|
|
40
|
-
const mockServerUrl = URL.createObjectURL(fileOrBlob)
|
|
41
|
-
console.log(`[模拟上传成功] 已返回服务器文件地址:`, mockServerUrl)
|
|
42
|
-
return mockServerUrl
|
|
43
|
-
}
|
|
44
|
-
</script>
|
|
45
|
-
|
|
46
|
-
<template>
|
|
47
|
-
<!-- :custom-upload="handleCustomUpload" -->
|
|
48
|
-
<PptEditor ref="editorRef" :custom-upload="handleCustomUpload" :enable-mcp="true" :enable-ai="true"
|
|
49
|
-
ai-api-url="/jvs-dam/api/ppt/ai-replace" :enable-material="true"
|
|
50
|
-
material-tree-api-url="/jvs-dam/api/dam/material/tree" material-page-api-url="/jvs-dam/api/dam/material/page"
|
|
51
|
-
material-api-url="/jvs-dam/api/dam/material/tree" material-save-api-url="/jvs-dam/api/dam/material/save"
|
|
52
|
-
@save="handleSave" @ready="handleReady" />
|
|
53
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import PptEditor from './components/PptEditor.vue'
|
|
4
|
+
import type { DeckData, PptEditorApi, UploadResourceContext } from './types'
|
|
5
|
+
|
|
6
|
+
const editorRef = ref<PptEditorApi | null>(null)
|
|
7
|
+
|
|
8
|
+
function handleSave(deck: DeckData) {
|
|
9
|
+
console.log('[JVS-PPT 独立运行模式] 保存幻灯片:', deck)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function handleReady(api: PptEditorApi) {
|
|
13
|
+
console.log('[JVS-PPT 独立运行模式] 编辑器初始化完成:', api)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 模拟资源上传函数
|
|
18
|
+
* 覆盖场景:导入 PPTX/JSON、上传图片、视频、音频、剪贴板粘贴、占位符上传、背景上传等
|
|
19
|
+
*/
|
|
20
|
+
async function handleCustomUpload(fileOrBlob: File | Blob, context: UploadResourceContext): Promise<string> {
|
|
21
|
+
console.log('[模拟自定义上传函数触发] 接收到待上传资源:', {
|
|
22
|
+
type: context.type, // 资源类型: 'image' | 'video' | 'audio'
|
|
23
|
+
source: context.source, // 触发来源: 'file-upload' | 'pptx-import' | 'json-import' | 'paste' | 'placeholder' | 'background'
|
|
24
|
+
filename: context.filename,
|
|
25
|
+
size: `${(fileOrBlob.size / 1024).toFixed(2)} KB`,
|
|
26
|
+
fileOrBlob
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// 1. 模拟网络上传请求耗时
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
31
|
+
|
|
32
|
+
// 2. 实际业务生产代码范例:
|
|
33
|
+
// const formData = new FormData()
|
|
34
|
+
// formData.append('file', fileOrBlob, context.filename || `upload_${Date.now()}`)
|
|
35
|
+
// formData.append('type', context.type)
|
|
36
|
+
// const res = await fetch('/api/oss/upload', { method: 'POST', body: formData }).then(r => r.json())
|
|
37
|
+
// return res.data.url
|
|
38
|
+
|
|
39
|
+
// 3. 模拟环境:生成可正常渲染的地址作为模拟的服务器返回 URL
|
|
40
|
+
const mockServerUrl = URL.createObjectURL(fileOrBlob)
|
|
41
|
+
console.log(`[模拟上传成功] 已返回服务器文件地址:`, mockServerUrl)
|
|
42
|
+
return mockServerUrl
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<!-- :custom-upload="handleCustomUpload" -->
|
|
48
|
+
<PptEditor ref="editorRef" :custom-upload="handleCustomUpload" :enable-mcp="true" :enable-ai="true"
|
|
49
|
+
ai-api-url="/jvs-dam/api/ppt/ai-replace" :enable-material="true"
|
|
50
|
+
material-tree-api-url="/jvs-dam/api/dam/material/tree" material-page-api-url="/jvs-dam/api/dam/material/page"
|
|
51
|
+
material-api-url="/jvs-dam/api/dam/material/tree" material-save-api-url="/jvs-dam/api/dam/material/save"
|
|
52
|
+
@save="handleSave" @ready="handleReady" />
|
|
53
|
+
</template>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { ref, watch, nextTick, onBeforeUnmount } from 'vue'
|
|
3
3
|
import { useSlides } from '../composables/useSlides'
|
|
4
4
|
import { compileSchemaToSlides } from '../utils/dslCompiler'
|
|
5
|
+
import { ensurePptxShapes } from '../utils/pptxShapesLazy'
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
visible: boolean
|
|
@@ -352,6 +353,7 @@ const startGenerating = async () => {
|
|
|
352
353
|
|
|
353
354
|
streamCodeText.value += `> PPTX 解析完成,正在绑定排版与动画...\n`
|
|
354
355
|
|
|
356
|
+
await ensurePptxShapes() // 解析 PPTist 原生格式前确保形状预设表就绪
|
|
355
357
|
let parsedResult: any = null
|
|
356
358
|
const currentParser = pptParserFn()
|
|
357
359
|
if (currentParser) {
|
|
@@ -3,7 +3,7 @@ import { ref, watch, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
|
|
3
3
|
import { useSlides, sanitizeNbspInHtml } from '../composables/useSlides'
|
|
4
4
|
import SvgChart from './SvgChart.vue'
|
|
5
5
|
import { SHAPE_LIST as CAT_SHAPE_LIST } from '../utils/shapes'
|
|
6
|
-
import {
|
|
6
|
+
import { shapeFormulas } from '../utils/pptxShapesLazy'
|
|
7
7
|
import { promoteListIndentation, restoreParagraphIndentation, collectBlockIndents, convertListElements, reapplyMissingListIndents, removeListStyle, mergeAdjacentSameTagLists, isContentListDominated, formatListStyleTypeForCss, snapSelectionStartToNextBlock, rangeTextLengthIn, collectBlockStyles, reapplyBlockStylesToListItems } from '../utils/listIndent'
|
|
8
8
|
import CanvasContextMenu from './canvas/CanvasContextMenu.vue'
|
|
9
9
|
import CanvasSmartGuides from './canvas/CanvasSmartGuides.vue'
|
|
@@ -19,7 +19,7 @@ import CanvasSymbolPanel from './canvas/panels/CanvasSymbolPanel.vue'
|
|
|
19
19
|
import CanvasSearchPanel from './canvas/panels/CanvasSearchPanel.vue'
|
|
20
20
|
import CanvasSelectionPanel from './canvas/panels/CanvasSelectionPanel.vue'
|
|
21
21
|
|
|
22
|
-
import { useCanvasTable } from '../composables/canvas/useCanvasTable'
|
|
22
|
+
import { useCanvasTable, cleanTableDOMForSaving } from '../composables/canvas/useCanvasTable'
|
|
23
23
|
import { useCanvasAlignment } from '../composables/canvas/useCanvasAlignment'
|
|
24
24
|
import { useCanvasShortcuts } from '../composables/canvas/useCanvasShortcuts'
|
|
25
25
|
import { useCanvasContextMenu } from '../composables/canvas/useCanvasContextMenu'
|
|
@@ -61,7 +61,7 @@ const {
|
|
|
61
61
|
activeSlide, selectedElementId, selectedElementIds, deleteElement, combineElements, uncombineElement, updateGroupBounds, zoomRatio, fitTrigger, activeTool, pendingTextType, pendingShapeSubtype, pendingShapeViewBox, pendingLineContent, activeIndex, slides, undo, redo, notify,
|
|
62
62
|
showSearchPanel, showSelectionPanel, activeSearchTab, searchText, replaceText, isCaseSensitive, searchResults, currentResultIndex,
|
|
63
63
|
nextSearchMatch, prevSearchMatch, replaceCurrent, replaceAll, formulaDialogVisible, currentEditingFormulaEl, renderLatexToSvg,
|
|
64
|
-
editingElementId, symbolPanelVisible, addElement, isPresenting, startPresentation, clipboard, slideClipboard, pasteSlideData, pasteElementsData, handleUnifiedPaste, shapeFormatPainter,
|
|
64
|
+
editingElementId, symbolPanelVisible, addElement, isPresenting, startPresentation, clipboard, slideClipboard, pasteSlideData, pasteElementsData, copySelectedElements, handleUnifiedPaste, shapeFormatPainter,
|
|
65
65
|
activeTab: activeTabRaw, isOpenedFromFloatToolbar, formatedAnimations, logicalWidth, logicalHeight, pauseHistory, isCanvasDragging, activeSlideVersion, addHistorySnapshot, normalizeHtmlFontFamily,
|
|
66
66
|
globalThemeColor, globalFontColor, globalFontFamily, globalBgColor, getActiveSlideThemeRecipe, croppingElementId,
|
|
67
67
|
isMasterEditingView, slideMasters, activeMasterId, activeLayoutId, activeLayout, sortSlideElements,
|
|
@@ -142,6 +142,26 @@ const viewportContainerRef = ref<HTMLElement | null>(null)
|
|
|
142
142
|
let viewportResizeObserver: ResizeObserver | null = null
|
|
143
143
|
const selectedElementWidth = ref('0px')
|
|
144
144
|
const selectedElementHeight = ref('0px')
|
|
145
|
+
|
|
146
|
+
// 拖拽期间全局图元尺寸缓存池:彻底消除拖动期间的 Forced Synchronous Layout(强制回流与 DOM 读取)
|
|
147
|
+
const activeDragDimensions = new Map<string, { width: number; height: number }>()
|
|
148
|
+
|
|
149
|
+
// 渲染期图元尺寸记忆化:模板对同一元素每渲染批会调 getElementWidthHeight 2-4 次,
|
|
150
|
+
// 签名含全部输入(style 尺寸/排版项、内容长度+首尾采样、选中 RO 尺寸),命中则免重复 DOM 测量
|
|
151
|
+
let elementDimCache = new WeakMap<object, { sig: string; dims: { width: number; height: number } }>()
|
|
152
|
+
watch(activeIndex, () => {
|
|
153
|
+
elementDimCache = new WeakMap()
|
|
154
|
+
cachedRenderElements = []
|
|
155
|
+
cachedRenderElementsSlideId = ''
|
|
156
|
+
cachedRenderElementsCount = -1
|
|
157
|
+
masterCacheMap.clear()
|
|
158
|
+
})
|
|
159
|
+
if (typeof document !== 'undefined' && (document as any).fonts?.ready) {
|
|
160
|
+
;(document as any).fonts.ready.then(() => {
|
|
161
|
+
elementDimCache = new WeakMap()
|
|
162
|
+
}).catch(() => {})
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
const isDraggingLine = ref(false)
|
|
146
166
|
const isDraggingChild = ref(false)
|
|
147
167
|
const isDraggingLineControlPoint = ref(false)
|
|
@@ -231,6 +251,7 @@ const {
|
|
|
231
251
|
selectElement: (id: string, e?: MouseEvent) => selectElement(id, e),
|
|
232
252
|
updateSelectionBorders: (tableEl: HTMLTableElement) => updateSelectionBorders(tableEl),
|
|
233
253
|
deleteElement: (id: string) => deleteElement(id),
|
|
254
|
+
copySelectedElements: () => copySelectedElements(),
|
|
234
255
|
handleUnifiedPaste: () => handleUnifiedPaste(),
|
|
235
256
|
getElementPixels: (el: any) => getElementPixels(el),
|
|
236
257
|
notify
|
|
@@ -344,7 +365,6 @@ const {
|
|
|
344
365
|
getSelectionBounds,
|
|
345
366
|
expandSelection,
|
|
346
367
|
updateSelectionBorders,
|
|
347
|
-
cleanTableDOMForSaving,
|
|
348
368
|
doSelectColumn,
|
|
349
369
|
selectColumnCells,
|
|
350
370
|
doSelectRow,
|
|
@@ -581,6 +601,8 @@ const {
|
|
|
581
601
|
logicalWidth,
|
|
582
602
|
logicalHeight,
|
|
583
603
|
getElementPixels: (el: any) => getElementPixels(el),
|
|
604
|
+
findElementInSlide: (id: any) => findElementInSlide(id),
|
|
605
|
+
findParentGroup: (id: any) => findParentGroup(id),
|
|
584
606
|
addHistorySnapshot
|
|
585
607
|
})
|
|
586
608
|
|
|
@@ -962,21 +984,6 @@ function getResizeCursor(handle: string, el: any) {
|
|
|
962
984
|
return getHandleCursor(handle, el?.style?.rotate || 0, !!el?.style?.flipH, !!el?.style?.flipV)
|
|
963
985
|
}
|
|
964
986
|
|
|
965
|
-
// 拖拽期间全局图元尺寸缓存池:彻底消除拖动期间的 Forced Synchronous Layout(强制回流与 DOM 读取)
|
|
966
|
-
const activeDragDimensions = new Map<string, { width: number; height: number }>()
|
|
967
|
-
|
|
968
|
-
// 渲染期图元尺寸记忆化:模板对同一元素每渲染批会调 getElementWidthHeight 2-4 次,
|
|
969
|
-
// 签名含全部输入(style 尺寸/排版项、内容长度+首尾采样、选中 RO 尺寸),命中则免重复 DOM 测量
|
|
970
|
-
let elementDimCache = new WeakMap<object, { sig: string; dims: { width: number; height: number } }>()
|
|
971
|
-
watch(activeIndex, () => {
|
|
972
|
-
elementDimCache = new WeakMap()
|
|
973
|
-
})
|
|
974
|
-
if (typeof document !== 'undefined' && (document as any).fonts?.ready) {
|
|
975
|
-
;(document as any).fonts.ready.then(() => {
|
|
976
|
-
elementDimCache = new WeakMap()
|
|
977
|
-
}).catch(() => {})
|
|
978
|
-
}
|
|
979
|
-
|
|
980
987
|
const getOverlayWidth = (el: any) => {
|
|
981
988
|
if (el.type === 'line') {
|
|
982
989
|
return el.style?.width || (el.width !== undefined ? `${el.width}px` : '1px')
|
|
@@ -1451,6 +1458,9 @@ function fitToScreen() {
|
|
|
1451
1458
|
const ch = viewportContainerRef.value.clientHeight
|
|
1452
1459
|
|
|
1453
1460
|
const padding = 60
|
|
1461
|
+
// 防御:若容器正在过渡动画或不可见时,clientWidth/Height 为 0 或 <= padding,此时绝不能计算并将 zoomRatio 设为 20%
|
|
1462
|
+
if (cw <= padding || ch <= padding) return
|
|
1463
|
+
|
|
1454
1464
|
const scale = Math.min((cw - padding) / logicalWidth.value, (ch - padding) / logicalHeight.value)
|
|
1455
1465
|
const targetZoom = Math.max(20, Math.min(200, Math.round(scale * 100)))
|
|
1456
1466
|
|
|
@@ -1551,6 +1561,17 @@ watch(selectedElementId, (newId, oldId) => {
|
|
|
1551
1561
|
}
|
|
1552
1562
|
if (oldEl.type === 'shape') oldEl.text = htmlVal
|
|
1553
1563
|
else {
|
|
1564
|
+
// 表格:退出编辑存档前剥离编辑态运行时属性(contenteditable/选区边框等),
|
|
1565
|
+
// 否则会烙进 content(退出后单元格仍可聚焦、蓝边框残留)
|
|
1566
|
+
if (htmlVal.includes('<table')) {
|
|
1567
|
+
const div = document.createElement('div')
|
|
1568
|
+
div.innerHTML = htmlVal
|
|
1569
|
+
const tableEl = div.querySelector('table')
|
|
1570
|
+
if (tableEl) {
|
|
1571
|
+
cleanTableDOMForSaving(tableEl)
|
|
1572
|
+
htmlVal = div.innerHTML
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1554
1575
|
oldEl.content = htmlVal
|
|
1555
1576
|
if (!isMasterOrPlaceholder(oldEl) && oldContainer.offsetHeight > 0) {
|
|
1556
1577
|
oldEl.height = `${oldContainer.offsetHeight}px`
|
|
@@ -1852,6 +1873,11 @@ onUnmounted(() => {
|
|
|
1852
1873
|
if (selectedElementResizeObserver) {
|
|
1853
1874
|
selectedElementResizeObserver.disconnect()
|
|
1854
1875
|
}
|
|
1876
|
+
cachedRenderElements = []
|
|
1877
|
+
cachedRenderElementsSlideId = ''
|
|
1878
|
+
cachedRenderElementsCount = -1
|
|
1879
|
+
masterCacheMap.clear()
|
|
1880
|
+
activeDragDimensions.clear()
|
|
1855
1881
|
})
|
|
1856
1882
|
|
|
1857
1883
|
function handleWindowMouseDown(e: MouseEvent) {
|
|
@@ -2763,7 +2789,7 @@ function handleCanvasMouseDown(e: MouseEvent) {
|
|
|
2763
2789
|
if (match.keypoints) {
|
|
2764
2790
|
keypoints = [...match.keypoints]
|
|
2765
2791
|
} else {
|
|
2766
|
-
const formulaObj =
|
|
2792
|
+
const formulaObj = shapeFormulas()[match.pathFormula]
|
|
2767
2793
|
if (formulaObj && formulaObj.defaultValue) {
|
|
2768
2794
|
keypoints = [...formulaObj.defaultValue]
|
|
2769
2795
|
}
|
|
@@ -2899,7 +2925,7 @@ function updateCreationPosition(e: MouseEvent) {
|
|
|
2899
2925
|
createdElement.style.width = `${widthPx.toFixed(0)}px`
|
|
2900
2926
|
createdElement.style.height = `${heightPx.toFixed(0)}px`
|
|
2901
2927
|
if (createdElement.pathFormula) {
|
|
2902
|
-
const formulaObj =
|
|
2928
|
+
const formulaObj = shapeFormulas()[createdElement.pathFormula]
|
|
2903
2929
|
if (formulaObj) {
|
|
2904
2930
|
const logicalW = parseFloat(createdElement.style.width) || 120
|
|
2905
2931
|
const logicalH = parseFloat(createdElement.style.height) || 120
|
|
@@ -3021,7 +3047,7 @@ function stopCreationDrag() {
|
|
|
3021
3047
|
if (match.keypoints) {
|
|
3022
3048
|
createdElement.keypoints = [...match.keypoints]
|
|
3023
3049
|
} else {
|
|
3024
|
-
const formulaObj =
|
|
3050
|
+
const formulaObj = shapeFormulas()[match.pathFormula]
|
|
3025
3051
|
if (formulaObj && formulaObj.defaultValue) {
|
|
3026
3052
|
createdElement.keypoints = [...formulaObj.defaultValue]
|
|
3027
3053
|
}
|
|
@@ -3029,7 +3055,7 @@ function stopCreationDrag() {
|
|
|
3029
3055
|
}
|
|
3030
3056
|
// If shape has path formula, generate initial correct path and viewBox
|
|
3031
3057
|
if (createdElement.pathFormula) {
|
|
3032
|
-
const formulaObj =
|
|
3058
|
+
const formulaObj = shapeFormulas()[createdElement.pathFormula]
|
|
3033
3059
|
if (formulaObj) {
|
|
3034
3060
|
const logicalW = parseFloat(createdElement.style.width) || 120
|
|
3035
3061
|
const logicalH = parseFloat(createdElement.style.height) || 120
|
|
@@ -3086,6 +3112,9 @@ function stopCreationDrag() {
|
|
|
3086
3112
|
}
|
|
3087
3113
|
|
|
3088
3114
|
activeSlide.value.elements.push(createdElement)
|
|
3115
|
+
if (createdElement.type === 'line' && (createdElement.startConnection || createdElement.endConnection)) {
|
|
3116
|
+
syncSlideConnections(activeSlide.value, logicalWidth.value, logicalHeight.value, [createdElement.id])
|
|
3117
|
+
}
|
|
3089
3118
|
selectedElementId.value = createdElement.id
|
|
3090
3119
|
selectedElementIds.value = [createdElement.id]
|
|
3091
3120
|
justCreatedElement = true
|
|
@@ -3628,12 +3657,25 @@ function handleElementDblClick(e: MouseEvent, el: any) {
|
|
|
3628
3657
|
target = activeSelectedEl
|
|
3629
3658
|
}
|
|
3630
3659
|
}
|
|
3660
|
+
// 已在编辑该元素时的双击 = 原生选词,保持选区不重进编辑
|
|
3661
|
+
// (重跑 startEditingShapeText 会覆盖 initialTextContent 变更基线)
|
|
3662
|
+
if (isEditingText.value && (selectedElementId.value === target.id || editingElementId?.value === target.id)) {
|
|
3663
|
+
return
|
|
3664
|
+
}
|
|
3631
3665
|
if (isImagePlaceholder(target)) {
|
|
3632
3666
|
triggerPlaceholderImageUpload(target.id)
|
|
3633
3667
|
} else if (target.type === 'shape') {
|
|
3634
3668
|
startEditingShapeText(target, e)
|
|
3635
3669
|
} else if (target.type === 'text') {
|
|
3636
|
-
|
|
3670
|
+
if (target.content?.includes?.('<table')) {
|
|
3671
|
+
// 表格元素必须走 handleTableDblClick(td 动态设 contenteditable);
|
|
3672
|
+
// startEditingTextElement 对表格无效——容器 contenteditable=false、焦点落
|
|
3673
|
+
// BODY,表现为"双击后进入了编辑态却打不了字"。真实双击常因两击间选中态
|
|
3674
|
+
// 重渲染被重定向到 .main-slide,经四层兜底进到这里
|
|
3675
|
+
handleTableDblClick(e, target)
|
|
3676
|
+
} else {
|
|
3677
|
+
startEditingTextElement(target.id, e)
|
|
3678
|
+
}
|
|
3637
3679
|
}
|
|
3638
3680
|
}
|
|
3639
3681
|
|
|
@@ -4033,6 +4075,10 @@ function handleChildClick(e: MouseEvent, child: any, parentGroup: any) {
|
|
|
4033
4075
|
function handleChildDblClick(e: MouseEvent, child: any, parentGroup: any) {
|
|
4034
4076
|
e.stopPropagation()
|
|
4035
4077
|
if (isLockedMasterElement(child)) return
|
|
4078
|
+
// 已在编辑该子元素时的双击 = 原生选词,保持选区不重进编辑
|
|
4079
|
+
if (isEditingText.value && (selectedElementId.value === child.id || editingElementId?.value === child.id)) {
|
|
4080
|
+
return
|
|
4081
|
+
}
|
|
4036
4082
|
if (child.type === 'group') {
|
|
4037
4083
|
const rootGroup = getRootParentGroup(child) || parentGroup
|
|
4038
4084
|
selectElement(rootGroup.id, e)
|
|
@@ -4127,7 +4173,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4127
4173
|
if (!el || typeof el !== 'object') return { width: 200, height: 100 }
|
|
4128
4174
|
|
|
4129
4175
|
// 拖拽中优先从尺寸缓存中直接获取,0 次 DOM 读取与回流
|
|
4130
|
-
if (isDragging.value && el.id && activeDragDimensions
|
|
4176
|
+
if (isDragging.value && el.id && typeof activeDragDimensions !== 'undefined' && activeDragDimensions?.has(el.id)) {
|
|
4131
4177
|
return activeDragDimensions.get(el.id)!
|
|
4132
4178
|
}
|
|
4133
4179
|
|
|
@@ -4161,7 +4207,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4161
4207
|
const selH = el.id === selectedElementId.value ? selectedElementHeight.value : ''
|
|
4162
4208
|
const contentSig = typeof el.content === 'string' ? `${el.content.length}|${el.content.slice(0, 12)}|${el.content.slice(-12)}` : ''
|
|
4163
4209
|
const dimSig = `${el.type}|${wStr}|${hStr}|${el.width || 0}|${el.height || 0}|${contentSig}|${el.latex || ''}|${st.fontSize || ''}|${st.fontFamily || ''}|${st.fontWeight || ''}|${st.lineHeight || ''}|${st.letterSpacing || ''}|${st.writingMode || ''}|${st.whiteSpace || ''}|${st.padding || ''}|${el.autoFit?.type || ''}|${el.autoFit?.fontScale || 0}|${selW}|${selH}|${logicalWidth.value}|${logicalHeight.value}|${isDragging.value ? 1 : 0}|${isResizing.value ? 1 : 0}`
|
|
4164
|
-
const dimCached = elementDimCache.get(el)
|
|
4210
|
+
const dimCached = (typeof elementDimCache !== 'undefined' && elementDimCache) ? elementDimCache.get(el) : undefined
|
|
4165
4211
|
if (dimCached && dimCached.sig === dimSig) return dimCached.dims
|
|
4166
4212
|
|
|
4167
4213
|
if (!isWPercent && !isHPercent && wStr && hStr && wStr !== 'auto' && hStr !== 'auto') {
|
|
@@ -4204,7 +4250,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4204
4250
|
// 关键:拖拽期间绝不查询 DOM,杜绝强制重排
|
|
4205
4251
|
const dom = isDragging.value ? null : (document.getElementById(`canvas-el-${el.id}`) || document.getElementById(`editable-${el.id}`))
|
|
4206
4252
|
|
|
4207
|
-
if (el.id === selectedElementId.value && !isResizing.value) {
|
|
4253
|
+
if (el.id === selectedElementId.value && !isResizing.value && el.type !== 'group') {
|
|
4208
4254
|
if (selectedElementWidth.value && selectedElementWidth.value !== '0px') {
|
|
4209
4255
|
const parsedW = parseFloat(selectedElementWidth.value)
|
|
4210
4256
|
if (!isNaN(parsedW) && parsedW > 0) width = parsedW
|
|
@@ -4260,7 +4306,9 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4260
4306
|
}
|
|
4261
4307
|
|
|
4262
4308
|
const dims = { width, height }
|
|
4263
|
-
|
|
4309
|
+
if (typeof elementDimCache !== 'undefined' && elementDimCache) {
|
|
4310
|
+
elementDimCache.set(el, { sig: dimSig, dims })
|
|
4311
|
+
}
|
|
4264
4312
|
return dims
|
|
4265
4313
|
}
|
|
4266
4314
|
|
|
@@ -4575,14 +4623,37 @@ function startDrag(e: MouseEvent, el: any) {
|
|
|
4575
4623
|
: selectedEls
|
|
4576
4624
|
|
|
4577
4625
|
// 检查是否有连线需要同步,避免无连线时每帧循环
|
|
4578
|
-
const selIds = new Set(selectedElementIds.value)
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4626
|
+
const selIds = new Set<string>(selectedElementIds.value)
|
|
4627
|
+
if (Array.isArray(targetIds)) targetIds.forEach(id => selIds.add(id))
|
|
4628
|
+
dragTargetEls.forEach((item: any) => {
|
|
4629
|
+
if (item?.id) selIds.add(item.id)
|
|
4630
|
+
if (item?.type === 'group' && Array.isArray(item.elements)) {
|
|
4631
|
+
const addChildren = (children: any[]) => {
|
|
4632
|
+
children.forEach(ch => {
|
|
4633
|
+
if (ch?.id) selIds.add(ch.id)
|
|
4634
|
+
if (ch?.type === 'group' && Array.isArray(ch.elements)) addChildren(ch.elements)
|
|
4635
|
+
})
|
|
4636
|
+
}
|
|
4637
|
+
addChildren(item.elements)
|
|
4638
|
+
}
|
|
4639
|
+
})
|
|
4640
|
+
const checkHasConnection = (elements: any[]): boolean => {
|
|
4641
|
+
for (const it of elements) {
|
|
4642
|
+
if (!it) continue
|
|
4643
|
+
if (it.type === 'line' && (
|
|
4644
|
+
selIds.has(it.id) ||
|
|
4645
|
+
(it.startConnection && selIds.has(it.startConnection.id)) ||
|
|
4646
|
+
(it.endConnection && selIds.has(it.endConnection.id))
|
|
4647
|
+
)) {
|
|
4648
|
+
return true
|
|
4649
|
+
}
|
|
4650
|
+
if (it.type === 'group' && Array.isArray(it.elements)) {
|
|
4651
|
+
if (checkHasConnection(it.elements)) return true
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
return false
|
|
4655
|
+
}
|
|
4656
|
+
hasConnectionsToSync = checkHasConnection(activeSlide.value?.elements || [])
|
|
4586
4657
|
|
|
4587
4658
|
// Record drag states
|
|
4588
4659
|
dragStates = dragTargetEls.map((item: any) => {
|
|
@@ -4828,7 +4899,8 @@ function stopDrag(e?: any) {
|
|
|
4828
4899
|
} finally {
|
|
4829
4900
|
isDragging.value = false
|
|
4830
4901
|
isCanvasDragging.value = false
|
|
4831
|
-
activeSlideVersion
|
|
4902
|
+
// 不再显式 bump activeSlideVersion:真实拖拽变更经下方 5000/5003 的 addHistorySnapshot
|
|
4903
|
+
// 落盘时统一 bump;纯点击选中无页面状态变化(选中态本就在画布 v-memo 签名里),无需全量失效
|
|
4832
4904
|
activeDragDimensions.clear()
|
|
4833
4905
|
hasConnectionsToSync = false
|
|
4834
4906
|
dragStates = []
|
|
@@ -4978,7 +5050,7 @@ const selectedElementKeypoints = computed(() => {
|
|
|
4978
5050
|
const el = selectedElement.value
|
|
4979
5051
|
if (!el.pathFormula || !el.keypoints || !Array.isArray(el.keypoints)) return []
|
|
4980
5052
|
|
|
4981
|
-
const formulaObj =
|
|
5053
|
+
const formulaObj = shapeFormulas()[el.pathFormula as string]
|
|
4982
5054
|
if (!formulaObj) return []
|
|
4983
5055
|
|
|
4984
5056
|
const geom = getElementPixels(el)
|
|
@@ -5487,7 +5559,8 @@ function changeQuickFontSize(el: any, size: string) {
|
|
|
5487
5559
|
}
|
|
5488
5560
|
}
|
|
5489
5561
|
size = formattedSize
|
|
5490
|
-
|
|
5562
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5563
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5491
5564
|
if (els.length === 0) return
|
|
5492
5565
|
|
|
5493
5566
|
// 1. First check if there are selected cells (cell-level selection)
|
|
@@ -5663,7 +5736,8 @@ function changeQuickTextAlign(el: any, align: string) {
|
|
|
5663
5736
|
}
|
|
5664
5737
|
|
|
5665
5738
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5666
|
-
|
|
5739
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5740
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5667
5741
|
els.forEach((item: any) => {
|
|
5668
5742
|
if (item.style) item.style.textAlign = align
|
|
5669
5743
|
syncHtmlAlign(item)
|
|
@@ -5678,7 +5752,8 @@ function changeQuickTextAlign(el: any, align: string) {
|
|
|
5678
5752
|
function changeQuickVerticalAlign(el: any, align: string) {
|
|
5679
5753
|
if (!el || !el.style) return
|
|
5680
5754
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5681
|
-
|
|
5755
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5756
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5682
5757
|
els.forEach((item: any) => {
|
|
5683
5758
|
if (item.style) item.style.verticalAlign = align
|
|
5684
5759
|
})
|
|
@@ -5692,7 +5767,8 @@ function toggleQuickBold(el: any) {
|
|
|
5692
5767
|
if (!el || !el.style) return
|
|
5693
5768
|
const targetState = el.style.fontWeight === 'bold' ? 'normal' : 'bold'
|
|
5694
5769
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5695
|
-
|
|
5770
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5771
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5696
5772
|
els.forEach((item: any) => {
|
|
5697
5773
|
if (item.style) item.style.fontWeight = targetState
|
|
5698
5774
|
})
|
|
@@ -5706,7 +5782,8 @@ function toggleQuickItalic(el: any) {
|
|
|
5706
5782
|
if (!el || !el.style) return
|
|
5707
5783
|
const targetState = el.style.fontStyle === 'italic' ? 'normal' : 'italic'
|
|
5708
5784
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5709
|
-
|
|
5785
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5786
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5710
5787
|
els.forEach((item: any) => {
|
|
5711
5788
|
if (item.style) item.style.fontStyle = targetState
|
|
5712
5789
|
})
|
|
@@ -5735,7 +5812,8 @@ function toggleQuickUnderline(el: any) {
|
|
|
5735
5812
|
const hasUnderline = (el.style.textDecoration || '').includes('underline')
|
|
5736
5813
|
const decorator = 'underline'
|
|
5737
5814
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5738
|
-
|
|
5815
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5816
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5739
5817
|
els.forEach((item: any) => {
|
|
5740
5818
|
if (item.style) {
|
|
5741
5819
|
const current = item.style.textDecoration || ''
|
|
@@ -5761,7 +5839,8 @@ function toggleQuickStrikethrough(el: any) {
|
|
|
5761
5839
|
const hasLineThrough = (el.style.textDecoration || '').includes('line-through')
|
|
5762
5840
|
const decorator = 'line-through'
|
|
5763
5841
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5764
|
-
|
|
5842
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5843
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5765
5844
|
els.forEach((item: any) => {
|
|
5766
5845
|
if (item.style) {
|
|
5767
5846
|
const current = item.style.textDecoration || ''
|
|
@@ -7206,16 +7285,13 @@ function handleGlobalCopy(e: ClipboardEvent) {
|
|
|
7206
7285
|
|
|
7207
7286
|
if (!activeSlide.value) return
|
|
7208
7287
|
|
|
7209
|
-
const
|
|
7210
|
-
|
|
7211
|
-
.
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
e.preventDefault()
|
|
7217
|
-
e.stopPropagation()
|
|
7218
|
-
notify(`已复制 ${copied.length} 个元素`)
|
|
7288
|
+
const payload = copySelectedElements()
|
|
7289
|
+
if (payload) {
|
|
7290
|
+
e.clipboardData?.setData('text/plain', JSON.stringify(payload))
|
|
7291
|
+
e.preventDefault()
|
|
7292
|
+
e.stopPropagation()
|
|
7293
|
+
notify(`已复制 ${payload.data.length} 个元素`)
|
|
7294
|
+
}
|
|
7219
7295
|
}
|
|
7220
7296
|
|
|
7221
7297
|
function handleGlobalCut(e: ClipboardEvent) {
|
|
@@ -7252,22 +7328,19 @@ function handleGlobalCut(e: ClipboardEvent) {
|
|
|
7252
7328
|
|
|
7253
7329
|
if (!activeSlide.value) return
|
|
7254
7330
|
|
|
7255
|
-
const
|
|
7256
|
-
|
|
7257
|
-
.
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
slideClipboard.value = null
|
|
7261
|
-
e.clipboardData?.setData('text/plain', JSON.stringify(copied))
|
|
7262
|
-
e.preventDefault()
|
|
7263
|
-
e.stopPropagation()
|
|
7331
|
+
const payload = copySelectedElements()
|
|
7332
|
+
if (payload) {
|
|
7333
|
+
e.clipboardData?.setData('text/plain', JSON.stringify(payload))
|
|
7334
|
+
e.preventDefault()
|
|
7335
|
+
e.stopPropagation()
|
|
7264
7336
|
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7337
|
+
selectedElementIds.value.forEach(id => {
|
|
7338
|
+
deleteElement(id)
|
|
7339
|
+
})
|
|
7340
|
+
selectedElementIds.value = []
|
|
7341
|
+
selectedElementId.value = null
|
|
7342
|
+
notify(`已剪切 ${payload.data.length} 个元素`)
|
|
7343
|
+
}
|
|
7271
7344
|
}
|
|
7272
7345
|
|
|
7273
7346
|
function extractImagesAndTextFromRawString(rawStr: string): { imageSources: string[]; remainingText: string } {
|
|
@@ -7428,6 +7501,14 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
7428
7501
|
pasteSlideData(data.data, activeIndex.value)
|
|
7429
7502
|
return
|
|
7430
7503
|
}
|
|
7504
|
+
if (data && data.type === 'elements' && Array.isArray(data.data) && data.data.length > 0) {
|
|
7505
|
+
e.preventDefault()
|
|
7506
|
+
e.stopPropagation()
|
|
7507
|
+
clipboard.value = data.data
|
|
7508
|
+
slideClipboard.value = null
|
|
7509
|
+
pasteElementsData(data.data, { sourceWidth: data.sourceWidth, sourceHeight: data.sourceHeight })
|
|
7510
|
+
return
|
|
7511
|
+
}
|
|
7431
7512
|
if (Array.isArray(data) && data.length > 0 && data[0].type && data[0].style) {
|
|
7432
7513
|
e.preventDefault()
|
|
7433
7514
|
e.stopPropagation()
|
|
@@ -7809,6 +7890,8 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
7809
7890
|
v-else-if="replica.el.type === 'group'"
|
|
7810
7891
|
:element="replica.el"
|
|
7811
7892
|
:active-slide="activeSlide"
|
|
7893
|
+
:parent-width-px="logicalWidth"
|
|
7894
|
+
:parent-height-px="logicalHeight"
|
|
7812
7895
|
:is-selected="false"
|
|
7813
7896
|
:selected-element-id="null"
|
|
7814
7897
|
:selected-child-id="null"
|
|
@@ -7920,8 +8003,12 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
7920
8003
|
v-show="el.visible !== false"
|
|
7921
8004
|
v-memo="[
|
|
7922
8005
|
(isDragging || isCanvasDragging)
|
|
7923
|
-
? (selectedElementIds.includes(el.id)
|
|
7924
|
-
|
|
8006
|
+
? (selectedElementIds.includes(el.id)
|
|
8007
|
+
? `${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.style?.rotate}`
|
|
8008
|
+
: (el.type === 'line'
|
|
8009
|
+
? `${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.start?.join(',')}_${el.end?.join(',')}_${el.broken?.join(',')}_${el.broken2?.join(',')}_${el.curve?.join(',')}`
|
|
8010
|
+
: 'frozen'))
|
|
8011
|
+
: `${el.id}_${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.style?.rotate}_${el.type === 'line' ? `${el.start?.join(',')}_${el.end?.join(',')}_${el.broken?.join(',')}_${el.broken2?.join(',')}_${el.curve?.join(',')}_` : ''}${selectedElementIds.includes(el.id)}_${selectedElementId === el.id}_${isEditingText}_${activeTab}_${activeSlideVersion}`
|
|
7925
8012
|
]"
|
|
7926
8013
|
:class="{
|
|
7927
8014
|
selected: selectedElementIds.includes(el.id),
|
|
@@ -8938,6 +9025,11 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
8938
9025
|
outline: none !important;
|
|
8939
9026
|
}
|
|
8940
9027
|
|
|
9028
|
+
/* 焦点(光标)所在的唯一选中格:不显示选中蒙层 */
|
|
9029
|
+
:deep(td.selected-cell.focused-cell) {
|
|
9030
|
+
background-image: none !important;
|
|
9031
|
+
}
|
|
9032
|
+
|
|
8941
9033
|
/* ================= 版式实时切换测试控制面板样式 ================= */
|
|
8942
9034
|
.layout-test-floating-panel {
|
|
8943
9035
|
position: absolute;
|