jvs-ppt-ui 0.0.9 → 0.0.11
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 +3 -0
- package/dist/lib/jvs-ppt-ui.es.js +22611 -21764
- package/dist/lib/jvs-ppt-ui.umd.js +102 -101
- package/dist/lib/style.css +1 -1
- package/package.json +7 -7
- package/scripts/check_connectors.cjs +25 -0
- package/scripts/check_slide1.cjs +11 -0
- package/scripts/check_slide123.cjs +25 -0
- package/scripts/convert-fonts-to-woff2.cjs +97 -0
- package/scripts/extract-slides.ts +149 -0
- package/scripts/find_candidate_pptx.cjs +34 -0
- package/scripts/find_orange_slide.cjs +29 -0
- package/scripts/generate-complete-ppt-preview.js +385 -0
- package/scripts/inspect_315_327.cjs +19 -0
- package/scripts/mcp-smoke-test.js +568 -0
- package/scripts/patch-pptxtojson.cjs +81 -0
- package/scripts/verify-fonts.cjs +20 -0
- package/src/App.vue +53 -53
- package/src/components/AIPptDialog.vue +2 -0
- package/src/components/CanvasArea.vue +124 -62
- package/src/components/Header.vue +364 -142
- package/src/components/PptEditor.vue +26 -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 +243 -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 +2 -2
- package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
- package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
- package/src/composables/useSlides.ts +482 -127
- package/src/style.css +88 -0
- package/src/types/index.ts +4 -0
- package/src/utils/pptxShapes.ts +1 -1
- package/src/utils/pptxShapesLazy.ts +39 -0
- package/src/utils/svgPathParser.ts +12 -1
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
|
|
@@ -3631,12 +3657,25 @@ function handleElementDblClick(e: MouseEvent, el: any) {
|
|
|
3631
3657
|
target = activeSelectedEl
|
|
3632
3658
|
}
|
|
3633
3659
|
}
|
|
3660
|
+
// 已在编辑该元素时的双击 = 原生选词,保持选区不重进编辑
|
|
3661
|
+
// (重跑 startEditingShapeText 会覆盖 initialTextContent 变更基线)
|
|
3662
|
+
if (isEditingText.value && (selectedElementId.value === target.id || editingElementId?.value === target.id)) {
|
|
3663
|
+
return
|
|
3664
|
+
}
|
|
3634
3665
|
if (isImagePlaceholder(target)) {
|
|
3635
3666
|
triggerPlaceholderImageUpload(target.id)
|
|
3636
3667
|
} else if (target.type === 'shape') {
|
|
3637
3668
|
startEditingShapeText(target, e)
|
|
3638
3669
|
} else if (target.type === 'text') {
|
|
3639
|
-
|
|
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
|
+
}
|
|
3640
3679
|
}
|
|
3641
3680
|
}
|
|
3642
3681
|
|
|
@@ -4036,6 +4075,10 @@ function handleChildClick(e: MouseEvent, child: any, parentGroup: any) {
|
|
|
4036
4075
|
function handleChildDblClick(e: MouseEvent, child: any, parentGroup: any) {
|
|
4037
4076
|
e.stopPropagation()
|
|
4038
4077
|
if (isLockedMasterElement(child)) return
|
|
4078
|
+
// 已在编辑该子元素时的双击 = 原生选词,保持选区不重进编辑
|
|
4079
|
+
if (isEditingText.value && (selectedElementId.value === child.id || editingElementId?.value === child.id)) {
|
|
4080
|
+
return
|
|
4081
|
+
}
|
|
4039
4082
|
if (child.type === 'group') {
|
|
4040
4083
|
const rootGroup = getRootParentGroup(child) || parentGroup
|
|
4041
4084
|
selectElement(rootGroup.id, e)
|
|
@@ -4130,7 +4173,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4130
4173
|
if (!el || typeof el !== 'object') return { width: 200, height: 100 }
|
|
4131
4174
|
|
|
4132
4175
|
// 拖拽中优先从尺寸缓存中直接获取,0 次 DOM 读取与回流
|
|
4133
|
-
if (isDragging.value && el.id && activeDragDimensions
|
|
4176
|
+
if (isDragging.value && el.id && typeof activeDragDimensions !== 'undefined' && activeDragDimensions?.has(el.id)) {
|
|
4134
4177
|
return activeDragDimensions.get(el.id)!
|
|
4135
4178
|
}
|
|
4136
4179
|
|
|
@@ -4164,7 +4207,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4164
4207
|
const selH = el.id === selectedElementId.value ? selectedElementHeight.value : ''
|
|
4165
4208
|
const contentSig = typeof el.content === 'string' ? `${el.content.length}|${el.content.slice(0, 12)}|${el.content.slice(-12)}` : ''
|
|
4166
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}`
|
|
4167
|
-
const dimCached = elementDimCache.get(el)
|
|
4210
|
+
const dimCached = (typeof elementDimCache !== 'undefined' && elementDimCache) ? elementDimCache.get(el) : undefined
|
|
4168
4211
|
if (dimCached && dimCached.sig === dimSig) return dimCached.dims
|
|
4169
4212
|
|
|
4170
4213
|
if (!isWPercent && !isHPercent && wStr && hStr && wStr !== 'auto' && hStr !== 'auto') {
|
|
@@ -4207,7 +4250,7 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4207
4250
|
// 关键:拖拽期间绝不查询 DOM,杜绝强制重排
|
|
4208
4251
|
const dom = isDragging.value ? null : (document.getElementById(`canvas-el-${el.id}`) || document.getElementById(`editable-${el.id}`))
|
|
4209
4252
|
|
|
4210
|
-
if (el.id === selectedElementId.value && !isResizing.value) {
|
|
4253
|
+
if (el.id === selectedElementId.value && !isResizing.value && el.type !== 'group') {
|
|
4211
4254
|
if (selectedElementWidth.value && selectedElementWidth.value !== '0px') {
|
|
4212
4255
|
const parsedW = parseFloat(selectedElementWidth.value)
|
|
4213
4256
|
if (!isNaN(parsedW) && parsedW > 0) width = parsedW
|
|
@@ -4263,7 +4306,9 @@ function getElementWidthHeight(el: any): { width: number; height: number } {
|
|
|
4263
4306
|
}
|
|
4264
4307
|
|
|
4265
4308
|
const dims = { width, height }
|
|
4266
|
-
|
|
4309
|
+
if (typeof elementDimCache !== 'undefined' && elementDimCache) {
|
|
4310
|
+
elementDimCache.set(el, { sig: dimSig, dims })
|
|
4311
|
+
}
|
|
4267
4312
|
return dims
|
|
4268
4313
|
}
|
|
4269
4314
|
|
|
@@ -4854,7 +4899,8 @@ function stopDrag(e?: any) {
|
|
|
4854
4899
|
} finally {
|
|
4855
4900
|
isDragging.value = false
|
|
4856
4901
|
isCanvasDragging.value = false
|
|
4857
|
-
activeSlideVersion
|
|
4902
|
+
// 不再显式 bump activeSlideVersion:真实拖拽变更经下方 5000/5003 的 addHistorySnapshot
|
|
4903
|
+
// 落盘时统一 bump;纯点击选中无页面状态变化(选中态本就在画布 v-memo 签名里),无需全量失效
|
|
4858
4904
|
activeDragDimensions.clear()
|
|
4859
4905
|
hasConnectionsToSync = false
|
|
4860
4906
|
dragStates = []
|
|
@@ -5004,7 +5050,7 @@ const selectedElementKeypoints = computed(() => {
|
|
|
5004
5050
|
const el = selectedElement.value
|
|
5005
5051
|
if (!el.pathFormula || !el.keypoints || !Array.isArray(el.keypoints)) return []
|
|
5006
5052
|
|
|
5007
|
-
const formulaObj =
|
|
5053
|
+
const formulaObj = shapeFormulas()[el.pathFormula as string]
|
|
5008
5054
|
if (!formulaObj) return []
|
|
5009
5055
|
|
|
5010
5056
|
const geom = getElementPixels(el)
|
|
@@ -5513,7 +5559,8 @@ function changeQuickFontSize(el: any, size: string) {
|
|
|
5513
5559
|
}
|
|
5514
5560
|
}
|
|
5515
5561
|
size = formattedSize
|
|
5516
|
-
|
|
5562
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5563
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5517
5564
|
if (els.length === 0) return
|
|
5518
5565
|
|
|
5519
5566
|
// 1. First check if there are selected cells (cell-level selection)
|
|
@@ -5689,7 +5736,8 @@ function changeQuickTextAlign(el: any, align: string) {
|
|
|
5689
5736
|
}
|
|
5690
5737
|
|
|
5691
5738
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5692
|
-
|
|
5739
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5740
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5693
5741
|
els.forEach((item: any) => {
|
|
5694
5742
|
if (item.style) item.style.textAlign = align
|
|
5695
5743
|
syncHtmlAlign(item)
|
|
@@ -5704,7 +5752,8 @@ function changeQuickTextAlign(el: any, align: string) {
|
|
|
5704
5752
|
function changeQuickVerticalAlign(el: any, align: string) {
|
|
5705
5753
|
if (!el || !el.style) return
|
|
5706
5754
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5707
|
-
|
|
5755
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5756
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5708
5757
|
els.forEach((item: any) => {
|
|
5709
5758
|
if (item.style) item.style.verticalAlign = align
|
|
5710
5759
|
})
|
|
@@ -5718,7 +5767,8 @@ function toggleQuickBold(el: any) {
|
|
|
5718
5767
|
if (!el || !el.style) return
|
|
5719
5768
|
const targetState = el.style.fontWeight === 'bold' ? 'normal' : 'bold'
|
|
5720
5769
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5721
|
-
|
|
5770
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5771
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5722
5772
|
els.forEach((item: any) => {
|
|
5723
5773
|
if (item.style) item.style.fontWeight = targetState
|
|
5724
5774
|
})
|
|
@@ -5732,7 +5782,8 @@ function toggleQuickItalic(el: any) {
|
|
|
5732
5782
|
if (!el || !el.style) return
|
|
5733
5783
|
const targetState = el.style.fontStyle === 'italic' ? 'normal' : 'italic'
|
|
5734
5784
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5735
|
-
|
|
5785
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5786
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5736
5787
|
els.forEach((item: any) => {
|
|
5737
5788
|
if (item.style) item.style.fontStyle = targetState
|
|
5738
5789
|
})
|
|
@@ -5761,7 +5812,8 @@ function toggleQuickUnderline(el: any) {
|
|
|
5761
5812
|
const hasUnderline = (el.style.textDecoration || '').includes('underline')
|
|
5762
5813
|
const decorator = 'underline'
|
|
5763
5814
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5764
|
-
|
|
5815
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5816
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5765
5817
|
els.forEach((item: any) => {
|
|
5766
5818
|
if (item.style) {
|
|
5767
5819
|
const current = item.style.textDecoration || ''
|
|
@@ -5787,7 +5839,8 @@ function toggleQuickStrikethrough(el: any) {
|
|
|
5787
5839
|
const hasLineThrough = (el.style.textDecoration || '').includes('line-through')
|
|
5788
5840
|
const decorator = 'line-through'
|
|
5789
5841
|
if (selectedElementIds.value.length > 1 && activeSlide.value) {
|
|
5790
|
-
|
|
5842
|
+
// 嵌套感知解析:组合内子元素也要能命中(findElementInSlide 走 elementMaps,含组内子元素)
|
|
5843
|
+
const els = selectedElementIds.value.map((id: string) => findElementInSlide(id)).filter((e: any) => Boolean(e))
|
|
5791
5844
|
els.forEach((item: any) => {
|
|
5792
5845
|
if (item.style) {
|
|
5793
5846
|
const current = item.style.textDecoration || ''
|
|
@@ -7232,16 +7285,13 @@ function handleGlobalCopy(e: ClipboardEvent) {
|
|
|
7232
7285
|
|
|
7233
7286
|
if (!activeSlide.value) return
|
|
7234
7287
|
|
|
7235
|
-
const
|
|
7236
|
-
|
|
7237
|
-
.
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
e.preventDefault()
|
|
7243
|
-
e.stopPropagation()
|
|
7244
|
-
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
|
+
}
|
|
7245
7295
|
}
|
|
7246
7296
|
|
|
7247
7297
|
function handleGlobalCut(e: ClipboardEvent) {
|
|
@@ -7278,22 +7328,19 @@ function handleGlobalCut(e: ClipboardEvent) {
|
|
|
7278
7328
|
|
|
7279
7329
|
if (!activeSlide.value) return
|
|
7280
7330
|
|
|
7281
|
-
const
|
|
7282
|
-
|
|
7283
|
-
.
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
slideClipboard.value = null
|
|
7287
|
-
e.clipboardData?.setData('text/plain', JSON.stringify(copied))
|
|
7288
|
-
e.preventDefault()
|
|
7289
|
-
e.stopPropagation()
|
|
7331
|
+
const payload = copySelectedElements()
|
|
7332
|
+
if (payload) {
|
|
7333
|
+
e.clipboardData?.setData('text/plain', JSON.stringify(payload))
|
|
7334
|
+
e.preventDefault()
|
|
7335
|
+
e.stopPropagation()
|
|
7290
7336
|
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7337
|
+
selectedElementIds.value.forEach(id => {
|
|
7338
|
+
deleteElement(id)
|
|
7339
|
+
})
|
|
7340
|
+
selectedElementIds.value = []
|
|
7341
|
+
selectedElementId.value = null
|
|
7342
|
+
notify(`已剪切 ${payload.data.length} 个元素`)
|
|
7343
|
+
}
|
|
7297
7344
|
}
|
|
7298
7345
|
|
|
7299
7346
|
function extractImagesAndTextFromRawString(rawStr: string): { imageSources: string[]; remainingText: string } {
|
|
@@ -7454,6 +7501,14 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
7454
7501
|
pasteSlideData(data.data, activeIndex.value)
|
|
7455
7502
|
return
|
|
7456
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
|
+
}
|
|
7457
7512
|
if (Array.isArray(data) && data.length > 0 && data[0].type && data[0].style) {
|
|
7458
7513
|
e.preventDefault()
|
|
7459
7514
|
e.stopPropagation()
|
|
@@ -7835,6 +7890,8 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
7835
7890
|
v-else-if="replica.el.type === 'group'"
|
|
7836
7891
|
:element="replica.el"
|
|
7837
7892
|
:active-slide="activeSlide"
|
|
7893
|
+
:parent-width-px="logicalWidth"
|
|
7894
|
+
:parent-height-px="logicalHeight"
|
|
7838
7895
|
:is-selected="false"
|
|
7839
7896
|
:selected-element-id="null"
|
|
7840
7897
|
:selected-child-id="null"
|
|
@@ -8968,6 +9025,11 @@ async function handleGlobalPaste(e: ClipboardEvent) {
|
|
|
8968
9025
|
outline: none !important;
|
|
8969
9026
|
}
|
|
8970
9027
|
|
|
9028
|
+
/* 焦点(光标)所在的唯一选中格:不显示选中蒙层 */
|
|
9029
|
+
:deep(td.selected-cell.focused-cell) {
|
|
9030
|
+
background-image: none !important;
|
|
9031
|
+
}
|
|
9032
|
+
|
|
8971
9033
|
/* ================= 版式实时切换测试控制面板样式 ================= */
|
|
8972
9034
|
.layout-test-floating-panel {
|
|
8973
9035
|
position: absolute;
|