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.
Files changed (34) hide show
  1. package/README.md +60 -2
  2. package/dist/lib/jvs-ppt-ui.es.js +22404 -21454
  3. package/dist/lib/jvs-ppt-ui.umd.js +102 -101
  4. package/dist/lib/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/App.vue +53 -53
  7. package/src/components/AIPptDialog.vue +2 -0
  8. package/src/components/CanvasArea.vue +164 -72
  9. package/src/components/Header.vue +374 -144
  10. package/src/components/PptEditor.vue +33 -15
  11. package/src/components/PresentationMode.vue +24 -4
  12. package/src/components/SidebarRight.vue +8 -1
  13. package/src/components/SvgChart.vue +5 -2
  14. package/src/components/TemplateGallery.vue +1321 -866
  15. package/src/components/Toolbar.vue +244 -22
  16. package/src/components/canvas/CanvasTransformHandles.vue +5 -1
  17. package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
  18. package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
  19. package/src/components/canvas/textSelectionOverlay.ts +305 -0
  20. package/src/composables/canvas/useCanvasAlignment.ts +286 -238
  21. package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
  22. package/src/composables/canvas/useCanvasFormatting.ts +35 -12
  23. package/src/composables/canvas/useCanvasTable.ts +52 -24
  24. package/src/composables/canvas/useCanvasText.ts +12 -0
  25. package/src/composables/canvas/useCanvasTransform.ts +15 -2
  26. package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
  27. package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
  28. package/src/composables/useSlides.ts +495 -132
  29. package/src/style.css +88 -0
  30. package/src/types/index.ts +8 -0
  31. package/src/utils/line.ts +274 -312
  32. package/src/utils/pptxShapes.ts +1 -1
  33. package/src/utils/pptxShapesLazy.ts +39 -0
  34. package/src/utils/svgPathParser.ts +12 -1
@@ -1,238 +1,286 @@
1
- import { type Ref } from 'vue'
2
-
3
- export interface AlignmentOptions {
4
- activeSlide: Ref<any>
5
- selectedElementIds: Ref<string[]>
6
- logicalWidth: Ref<number>
7
- logicalHeight: Ref<number>
8
- getElementPixels: (el: any) => { left: number; top: number; width: number; height: number; rotate: number }
9
- addHistorySnapshot: () => void
10
- }
11
-
12
- interface GeomItem {
13
- el: any
14
- left: number
15
- top: number
16
- width: number
17
- height: number
18
- rotate: number
19
- }
20
-
21
- export function useCanvasAlignment(options: AlignmentOptions) {
22
- const {
23
- activeSlide,
24
- selectedElementIds,
25
- logicalWidth,
26
- logicalHeight,
27
- getElementPixels,
28
- addHistorySnapshot
29
- } = options
30
-
31
- function getSelectedItemsGeom(): GeomItem[] {
32
- if (!activeSlide.value) return []
33
- return activeSlide.value.elements
34
- .filter((el: any) => selectedElementIds.value.includes(el.id))
35
- .map((el: any) => ({
36
- el,
37
- ...getElementPixels(el)
38
- }))
39
- }
40
-
41
- function quickAlignLeft() {
42
- const items = getSelectedItemsGeom()
43
- if (items.length === 0) return
44
- if (items.length === 1) {
45
- items[0].el.style.left = '0%'
46
- addHistorySnapshot()
47
- return
48
- }
49
- const minLeft = Math.min(...items.map((i: GeomItem) => i.left))
50
- items.forEach((item: GeomItem) => {
51
- const pct = (minLeft / logicalWidth.value) * 100
52
- item.el.style.left = `${pct.toFixed(2)}%`
53
- })
54
- addHistorySnapshot()
55
- }
56
-
57
- function quickAlignCenter() {
58
- const items = getSelectedItemsGeom()
59
- if (items.length === 0) return
60
- if (items.length === 1) {
61
- const left = (logicalWidth.value - items[0].width) / 2
62
- const pct = (left / logicalWidth.value) * 100
63
- items[0].el.style.left = `${pct.toFixed(2)}%`
64
- addHistorySnapshot()
65
- return
66
- }
67
- const minLeft = Math.min(...items.map((i: GeomItem) => i.left))
68
- const maxRight = Math.max(...items.map((i: GeomItem) => i.left + i.width))
69
- const center = (minLeft + maxRight) / 2
70
- items.forEach((item: GeomItem) => {
71
- const left = center - item.width / 2
72
- const pct = (left / logicalWidth.value) * 100
73
- item.el.style.left = `${pct.toFixed(2)}%`
74
- })
75
- addHistorySnapshot()
76
- }
77
-
78
- function quickAlignRight() {
79
- const items = getSelectedItemsGeom()
80
- if (items.length === 0) return
81
- if (items.length === 1) {
82
- const left = logicalWidth.value - items[0].width
83
- const pct = (left / logicalWidth.value) * 100
84
- items[0].el.style.left = `${pct.toFixed(2)}%`
85
- addHistorySnapshot()
86
- return
87
- }
88
- const maxRight = Math.max(...items.map((i: GeomItem) => i.left + i.width))
89
- items.forEach((item: GeomItem) => {
90
- const left = maxRight - item.width
91
- const pct = (left / logicalWidth.value) * 100
92
- item.el.style.left = `${pct.toFixed(2)}%`
93
- })
94
- addHistorySnapshot()
95
- }
96
-
97
- function quickAlignTop() {
98
- const items = getSelectedItemsGeom()
99
- if (items.length === 0) return
100
- if (items.length === 1) {
101
- items[0].el.style.top = '0%'
102
- addHistorySnapshot()
103
- return
104
- }
105
- const minTop = Math.min(...items.map((i: GeomItem) => i.top))
106
- items.forEach((item: GeomItem) => {
107
- const pct = (minTop / logicalHeight.value) * 100
108
- item.el.style.top = `${pct.toFixed(2)}%`
109
- })
110
- addHistorySnapshot()
111
- }
112
-
113
- function quickAlignMiddle() {
114
- const items = getSelectedItemsGeom()
115
- if (items.length === 0) return
116
- if (items.length === 1) {
117
- const top = (logicalHeight.value - items[0].height) / 2
118
- const pct = (top / logicalHeight.value) * 100
119
- items[0].el.style.top = `${pct.toFixed(2)}%`
120
- addHistorySnapshot()
121
- return
122
- }
123
- const minTop = Math.min(...items.map((i: GeomItem) => i.top))
124
- const maxBottom = Math.max(...items.map((i: GeomItem) => i.top + i.height))
125
- const middle = (minTop + maxBottom) / 2
126
- items.forEach((item: GeomItem) => {
127
- const top = middle - item.height / 2
128
- const pct = (top / logicalHeight.value) * 100
129
- item.el.style.top = `${pct.toFixed(2)}%`
130
- })
131
- addHistorySnapshot()
132
- }
133
-
134
- function quickAlignBottom() {
135
- const items = getSelectedItemsGeom()
136
- if (items.length === 0) return
137
- if (items.length === 1) {
138
- const top = logicalHeight.value - items[0].height
139
- const pct = (top / logicalHeight.value) * 100
140
- items[0].el.style.top = `${pct.toFixed(2)}%`
141
- addHistorySnapshot()
142
- return
143
- }
144
- const maxBottom = Math.max(...items.map((i: GeomItem) => i.top + i.height))
145
- items.forEach((item: GeomItem) => {
146
- const top = maxBottom - item.height
147
- const pct = (top / logicalHeight.value) * 100
148
- item.el.style.top = `${pct.toFixed(2)}%`
149
- })
150
- addHistorySnapshot()
151
- }
152
-
153
- function quickDistributeHorizontally() {
154
- const items = getSelectedItemsGeom().sort((a: GeomItem, b: GeomItem) => a.left - b.left)
155
- if (items.length < 2) return
156
-
157
- if (items.length === 2) {
158
- const totalWidths = items[0].width + items[1].width
159
- const gap = (logicalWidth.value - totalWidths) / 3
160
- const left1Pct = (gap / logicalWidth.value) * 100
161
- const left2Pct = ((gap * 2 + items[0].width) / logicalWidth.value) * 100
162
- items[0].el.style.left = `${left1Pct.toFixed(2)}%`
163
- items[1].el.style.left = `${left2Pct.toFixed(2)}%`
164
- addHistorySnapshot()
165
- return
166
- }
167
-
168
- const minLeft = items[0].left
169
- const maxRight = items[items.length - 1].left + items[items.length - 1].width
170
- const totalWidths = items.reduce((sum: number, item: GeomItem) => sum + item.width, 0)
171
- const availableWidth = maxRight - minLeft
172
- const gap = (availableWidth - totalWidths) / (items.length - 1)
173
-
174
- let currentLeft = minLeft
175
- items.forEach((item: GeomItem, index: number) => {
176
- if (index === 0) {
177
- currentLeft += item.width + gap
178
- return
179
- }
180
- if (index === items.length - 1) {
181
- return
182
- }
183
- const pct = (currentLeft / logicalWidth.value) * 100
184
- item.el.style.left = `${pct.toFixed(2)}%`
185
- currentLeft += item.width + gap
186
- })
187
- addHistorySnapshot()
188
- }
189
-
190
- function quickDistributeVertically() {
191
- const items = getSelectedItemsGeom().sort((a: GeomItem, b: GeomItem) => a.top - b.top)
192
- if (items.length < 2) return
193
-
194
- if (items.length === 2) {
195
- const totalHeights = items[0].height + items[1].height
196
- const gap = (logicalHeight.value - totalHeights) / 3
197
- const top1Pct = (gap / logicalHeight.value) * 100
198
- const top2Pct = ((gap * 2 + items[0].height) / logicalHeight.value) * 100
199
- items[0].el.style.top = `${top1Pct.toFixed(2)}%`
200
- items[1].el.style.top = `${top2Pct.toFixed(2)}%`
201
- addHistorySnapshot()
202
- return
203
- }
204
-
205
- const minTop = items[0].top
206
- const maxBottom = items[items.length - 1].top + items[items.length - 1].height
207
- const totalHeights = items.reduce((sum: number, item: GeomItem) => sum + item.height, 0)
208
- const availableHeight = maxBottom - minTop
209
- const gap = (availableHeight - totalHeights) / (items.length - 1)
210
-
211
- let currentTop = minTop
212
- items.forEach((item: GeomItem, index: number) => {
213
- if (index === 0) {
214
- currentTop += item.height + gap
215
- return
216
- }
217
- if (index === items.length - 1) {
218
- return
219
- }
220
- const pct = (currentTop / logicalHeight.value) * 100
221
- item.el.style.top = `${pct.toFixed(2)}%`
222
- currentTop += item.height + gap
223
- })
224
- addHistorySnapshot()
225
- }
226
-
227
- return {
228
- getSelectedItemsGeom,
229
- quickAlignLeft,
230
- quickAlignCenter,
231
- quickAlignRight,
232
- quickAlignTop,
233
- quickAlignMiddle,
234
- quickAlignBottom,
235
- quickDistributeHorizontally,
236
- quickDistributeVertically
237
- }
238
- }
1
+ import { type Ref } from 'vue'
2
+
3
+ export interface AlignmentOptions {
4
+ activeSlide: Ref<any>
5
+ selectedElementIds: Ref<string[]>
6
+ logicalWidth: Ref<number>
7
+ logicalHeight: Ref<number>
8
+ getElementPixels: (el: any) => { left: number; top: number; width: number; height: number; rotate: number }
9
+ findElementInSlide?: (id: any) => any
10
+ findParentGroup?: (id: any) => any
11
+ addHistorySnapshot: () => void
12
+ }
13
+
14
+ interface GeomItem {
15
+ el: any
16
+ left: number
17
+ top: number
18
+ width: number
19
+ height: number
20
+ rotate: number
21
+ }
22
+
23
+ interface Box {
24
+ left: number
25
+ top: number
26
+ width: number
27
+ height: number
28
+ }
29
+
30
+ export function useCanvasAlignment(options: AlignmentOptions) {
31
+ const {
32
+ activeSlide,
33
+ selectedElementIds,
34
+ logicalWidth,
35
+ logicalHeight,
36
+ getElementPixels,
37
+ findElementInSlide,
38
+ findParentGroup,
39
+ addHistorySnapshot
40
+ } = options
41
+
42
+ // 选中集可能是顶层元素,也可能是组合内子元素(id 不会出现在 activeSlide.elements 顶层),
43
+ // 需要经 findElementInSlide 递归解析;祖先组合与后代同时选中时只保留祖先,避免双重位移
44
+ function resolveSelectedElements(): any[] {
45
+ const ids = selectedElementIds.value
46
+ if (!activeSlide.value || ids.length === 0) return []
47
+ let found: any[]
48
+ if (findElementInSlide) {
49
+ found = ids.map((id: any) => findElementInSlide(id)).filter(Boolean) as any[]
50
+ } else {
51
+ found = activeSlide.value.elements.filter((el: any) => ids.includes(el.id))
52
+ }
53
+ if (found.length === 0 || !findParentGroup) return found
54
+ return found.filter((el: any) => {
55
+ let parent = findParentGroup(el.id)
56
+ while (parent) {
57
+ if (found.includes(parent)) return false
58
+ parent = findParentGroup(parent.id)
59
+ }
60
+ return true
61
+ })
62
+ }
63
+
64
+ function getSelectedItemsGeom(): GeomItem[] {
65
+ return resolveSelectedElements().map((el: any) => ({
66
+ el,
67
+ ...getElementPixels(el)
68
+ }))
69
+ }
70
+
71
+ // 单元素对齐 / 两元素分布的参照容器:
72
+ // 组内子元素以父组合盒子为容器(对齐到组合边缘而不是画布边缘),其余以画布为容器
73
+ function getItemsContainer(items: GeomItem[]): Box {
74
+ const parents = new Set<any>()
75
+ items.forEach((item: GeomItem) => {
76
+ const parent = findParentGroup ? findParentGroup(item.el.id) : null
77
+ if (parent) parents.add(parent)
78
+ })
79
+ if (parents.size === 1) {
80
+ const parent = [...parents][0]
81
+ const pp = getElementPixels(parent)
82
+ if (pp.width > 0 && pp.height > 0) return { left: pp.left, top: pp.top, width: pp.width, height: pp.height }
83
+ }
84
+ return { left: 0, top: 0, width: logicalWidth.value, height: logicalHeight.value }
85
+ }
86
+
87
+ // 绝对坐标写回:组内子元素的 style.left/top 是相对父组合盒子的百分比,
88
+ // 必须减去父组合绝对偏移再除以父组合尺寸(与 getElementPixels 的解析互逆)
89
+ function setItemLeft(item: GeomItem, absLeft: number) {
90
+ const parent = findParentGroup ? findParentGroup(item.el.id) : null
91
+ if (parent) {
92
+ const pp = getElementPixels(parent)
93
+ const base = pp.width || logicalWidth.value
94
+ item.el.style.left = `${(((absLeft - pp.left) / base) * 100).toFixed(2)}%`
95
+ } else {
96
+ item.el.style.left = `${((absLeft / logicalWidth.value) * 100).toFixed(2)}%`
97
+ }
98
+ }
99
+
100
+ function setItemTop(item: GeomItem, absTop: number) {
101
+ const parent = findParentGroup ? findParentGroup(item.el.id) : null
102
+ if (parent) {
103
+ const pp = getElementPixels(parent)
104
+ const base = pp.height || logicalHeight.value
105
+ item.el.style.top = `${(((absTop - pp.top) / base) * 100).toFixed(2)}%`
106
+ } else {
107
+ item.el.style.top = `${((absTop / logicalHeight.value) * 100).toFixed(2)}%`
108
+ }
109
+ }
110
+
111
+ function quickAlignLeft() {
112
+ const items = getSelectedItemsGeom()
113
+ if (items.length === 0) return
114
+ if (items.length === 1) {
115
+ setItemLeft(items[0], getItemsContainer(items).left)
116
+ addHistorySnapshot()
117
+ return
118
+ }
119
+ const minLeft = Math.min(...items.map((i: GeomItem) => i.left))
120
+ items.forEach((item: GeomItem) => setItemLeft(item, minLeft))
121
+ addHistorySnapshot()
122
+ }
123
+
124
+ function quickAlignCenter() {
125
+ const items = getSelectedItemsGeom()
126
+ if (items.length === 0) return
127
+ if (items.length === 1) {
128
+ const box = getItemsContainer(items)
129
+ setItemLeft(items[0], box.left + (box.width - items[0].width) / 2)
130
+ addHistorySnapshot()
131
+ return
132
+ }
133
+ const minLeft = Math.min(...items.map((i: GeomItem) => i.left))
134
+ const maxRight = Math.max(...items.map((i: GeomItem) => i.left + i.width))
135
+ const center = (minLeft + maxRight) / 2
136
+ items.forEach((item: GeomItem) => {
137
+ setItemLeft(item, center - item.width / 2)
138
+ })
139
+ addHistorySnapshot()
140
+ }
141
+
142
+ function quickAlignRight() {
143
+ const items = getSelectedItemsGeom()
144
+ if (items.length === 0) return
145
+ if (items.length === 1) {
146
+ const box = getItemsContainer(items)
147
+ setItemLeft(items[0], box.left + box.width - items[0].width)
148
+ addHistorySnapshot()
149
+ return
150
+ }
151
+ const maxRight = Math.max(...items.map((i: GeomItem) => i.left + i.width))
152
+ items.forEach((item: GeomItem) => {
153
+ setItemLeft(item, maxRight - item.width)
154
+ })
155
+ addHistorySnapshot()
156
+ }
157
+
158
+ function quickAlignTop() {
159
+ const items = getSelectedItemsGeom()
160
+ if (items.length === 0) return
161
+ if (items.length === 1) {
162
+ setItemTop(items[0], getItemsContainer(items).top)
163
+ addHistorySnapshot()
164
+ return
165
+ }
166
+ const minTop = Math.min(...items.map((i: GeomItem) => i.top))
167
+ items.forEach((item: GeomItem) => setItemTop(item, minTop))
168
+ addHistorySnapshot()
169
+ }
170
+
171
+ function quickAlignMiddle() {
172
+ const items = getSelectedItemsGeom()
173
+ if (items.length === 0) return
174
+ if (items.length === 1) {
175
+ const box = getItemsContainer(items)
176
+ setItemTop(items[0], box.top + (box.height - items[0].height) / 2)
177
+ addHistorySnapshot()
178
+ return
179
+ }
180
+ const minTop = Math.min(...items.map((i: GeomItem) => i.top))
181
+ const maxBottom = Math.max(...items.map((i: GeomItem) => i.top + i.height))
182
+ const middle = (minTop + maxBottom) / 2
183
+ items.forEach((item: GeomItem) => {
184
+ setItemTop(item, middle - item.height / 2)
185
+ })
186
+ addHistorySnapshot()
187
+ }
188
+
189
+ function quickAlignBottom() {
190
+ const items = getSelectedItemsGeom()
191
+ if (items.length === 0) return
192
+ if (items.length === 1) {
193
+ const box = getItemsContainer(items)
194
+ setItemTop(items[0], box.top + box.height - items[0].height)
195
+ addHistorySnapshot()
196
+ return
197
+ }
198
+ const maxBottom = Math.max(...items.map((i: GeomItem) => i.top + i.height))
199
+ items.forEach((item: GeomItem) => {
200
+ setItemTop(item, maxBottom - item.height)
201
+ })
202
+ addHistorySnapshot()
203
+ }
204
+
205
+ function quickDistributeHorizontally() {
206
+ const items = getSelectedItemsGeom().sort((a: GeomItem, b: GeomItem) => a.left - b.left)
207
+ if (items.length < 2) return
208
+
209
+ if (items.length === 2) {
210
+ const box = getItemsContainer(items)
211
+ const totalWidths = items[0].width + items[1].width
212
+ const gap = (box.width - totalWidths) / 3
213
+ setItemLeft(items[0], box.left + gap)
214
+ setItemLeft(items[1], box.left + gap * 2 + items[0].width)
215
+ addHistorySnapshot()
216
+ return
217
+ }
218
+
219
+ const minLeft = items[0].left
220
+ const maxRight = items[items.length - 1].left + items[items.length - 1].width
221
+ const totalWidths = items.reduce((sum: number, item: GeomItem) => sum + item.width, 0)
222
+ const availableWidth = maxRight - minLeft
223
+ const gap = (availableWidth - totalWidths) / (items.length - 1)
224
+
225
+ let currentLeft = minLeft
226
+ items.forEach((item: GeomItem, index: number) => {
227
+ if (index === 0) {
228
+ currentLeft += item.width + gap
229
+ return
230
+ }
231
+ if (index === items.length - 1) {
232
+ return
233
+ }
234
+ setItemLeft(item, currentLeft)
235
+ currentLeft += item.width + gap
236
+ })
237
+ addHistorySnapshot()
238
+ }
239
+
240
+ function quickDistributeVertically() {
241
+ const items = getSelectedItemsGeom().sort((a: GeomItem, b: GeomItem) => a.top - b.top)
242
+ if (items.length < 2) return
243
+
244
+ if (items.length === 2) {
245
+ const box = getItemsContainer(items)
246
+ const totalHeights = items[0].height + items[1].height
247
+ const gap = (box.height - totalHeights) / 3
248
+ setItemTop(items[0], box.top + gap)
249
+ setItemTop(items[1], box.top + gap * 2 + items[0].height)
250
+ addHistorySnapshot()
251
+ return
252
+ }
253
+
254
+ const minTop = items[0].top
255
+ const maxBottom = items[items.length - 1].top + items[items.length - 1].height
256
+ const totalHeights = items.reduce((sum: number, item: GeomItem) => sum + item.height, 0)
257
+ const availableHeight = maxBottom - minTop
258
+ const gap = (availableHeight - totalHeights) / (items.length - 1)
259
+
260
+ let currentTop = minTop
261
+ items.forEach((item: GeomItem, index: number) => {
262
+ if (index === 0) {
263
+ currentTop += item.height + gap
264
+ return
265
+ }
266
+ if (index === items.length - 1) {
267
+ return
268
+ }
269
+ setItemTop(item, currentTop)
270
+ currentTop += item.height + gap
271
+ })
272
+ addHistorySnapshot()
273
+ }
274
+
275
+ return {
276
+ getSelectedItemsGeom,
277
+ quickAlignLeft,
278
+ quickAlignCenter,
279
+ quickAlignRight,
280
+ quickAlignTop,
281
+ quickAlignMiddle,
282
+ quickAlignBottom,
283
+ quickDistributeHorizontally,
284
+ quickDistributeVertically
285
+ }
286
+ }
@@ -27,6 +27,7 @@ export interface ContextMenuOptions {
27
27
  selectElement: (id: string, e?: MouseEvent) => void
28
28
  updateSelectionBorders: (tableEl: HTMLTableElement) => void
29
29
  deleteElement: (id: string) => void
30
+ copySelectedElements?: () => { type: 'elements'; data: any[]; sourceWidth: number; sourceHeight: number } | null
30
31
  handleUnifiedPaste: () => Promise<void | boolean>
31
32
  getElementPixels: (el: any) => { left: number; top: number; width: number; height: number; rotate: number }
32
33
  notify: (msg: string) => void
@@ -50,6 +51,7 @@ export function useCanvasContextMenu(options: ContextMenuOptions) {
50
51
  selectElement,
51
52
  updateSelectionBorders,
52
53
  deleteElement,
54
+ copySelectedElements,
53
55
  handleUnifiedPaste,
54
56
  getElementPixels,
55
57
  notify
@@ -326,15 +328,28 @@ export function useCanvasContextMenu(options: ContextMenuOptions) {
326
328
  function copySelected() {
327
329
  if (selectedElementIds.value.length === 0) return
328
330
  if (!activeSlide.value) return
329
- const copied = getElementsByIds(selectedElementIds.value).map((el: any) => JSON.parse(JSON.stringify(el)))
330
- clipboard.value = copied
331
- slideClipboard.value = null
332
- try {
333
- navigator.clipboard.writeText(JSON.stringify(copied))
334
- } catch (err) {
335
- // Ignore
331
+
332
+ if (copySelectedElements) {
333
+ const payload = copySelectedElements()
334
+ if (payload) {
335
+ try {
336
+ navigator.clipboard.writeText(JSON.stringify(payload))
337
+ } catch (err) {
338
+ // Ignore
339
+ }
340
+ notify(`已复制 ${payload.data.length} 个元素`)
341
+ }
342
+ } else {
343
+ const copied = getElementsByIds(selectedElementIds.value).map((el: any) => JSON.parse(JSON.stringify(el)))
344
+ clipboard.value = copied
345
+ slideClipboard.value = null
346
+ try {
347
+ navigator.clipboard.writeText(JSON.stringify(copied))
348
+ } catch (err) {
349
+ // Ignore
350
+ }
351
+ notify(`已复制 ${copied.length} 个元素`)
336
352
  }
337
- notify(`已复制 ${copied.length} 个元素`)
338
353
  showContextMenu.value = false
339
354
  }
340
355
 
@@ -342,21 +357,37 @@ export function useCanvasContextMenu(options: ContextMenuOptions) {
342
357
  if (selectedElementIds.value.length === 0) return
343
358
  if (!activeSlide.value) return
344
359
 
345
- const copied = getElementsByIds(selectedElementIds.value).map((el: any) => JSON.parse(JSON.stringify(el)))
346
- clipboard.value = copied
347
- slideClipboard.value = null
348
- try {
349
- navigator.clipboard.writeText(JSON.stringify(copied))
350
- } catch (err) {
351
- // Ignore
360
+ if (copySelectedElements) {
361
+ const payload = copySelectedElements()
362
+ if (payload) {
363
+ try {
364
+ navigator.clipboard.writeText(JSON.stringify(payload))
365
+ } catch (err) {
366
+ // Ignore
367
+ }
368
+ selectedElementIds.value.forEach(id => {
369
+ deleteElement(id)
370
+ })
371
+ selectedElementIds.value = []
372
+ selectedElementId.value = null
373
+ notify(`已剪切 ${payload.data.length} 个元素`)
374
+ }
375
+ } else {
376
+ const copied = getElementsByIds(selectedElementIds.value).map((el: any) => JSON.parse(JSON.stringify(el)))
377
+ clipboard.value = copied
378
+ slideClipboard.value = null
379
+ try {
380
+ navigator.clipboard.writeText(JSON.stringify(copied))
381
+ } catch (err) {
382
+ // Ignore
383
+ }
384
+ selectedElementIds.value.forEach(id => {
385
+ deleteElement(id)
386
+ })
387
+ selectedElementIds.value = []
388
+ selectedElementId.value = null
389
+ notify(`已剪切 ${copied.length} 个元素`)
352
390
  }
353
-
354
- selectedElementIds.value.forEach(id => {
355
- deleteElement(id)
356
- })
357
- selectedElementIds.value = []
358
- selectedElementId.value = null
359
- notify(`已剪切 ${copied.length} 个元素`)
360
391
  showContextMenu.value = false
361
392
  }
362
393