jvs-ppt-ui 0.0.14 → 0.0.16

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 (67) hide show
  1. package/dist/lib/check_imgs.html +18 -0
  2. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
  3. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
  4. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
  5. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
  6. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
  7. package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
  8. package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
  9. package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
  10. package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
  11. package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
  12. package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
  13. package/dist/lib/jvs-ppt-ui.umd.js +102 -102
  14. package/dist/lib/style.css +1 -1
  15. package/package.json +1 -1
  16. package/scripts/check-icons.mjs +22 -0
  17. package/scripts/patch-anim-icons.mjs +45 -0
  18. package/src/assets/iconfont/iconfont.css +187 -27
  19. package/src/assets/iconfont/iconfont.js +1 -1
  20. package/src/assets/iconfont/iconfont.json +345 -65
  21. package/src/assets/iconfont/iconfont.ttf +0 -0
  22. package/src/assets/iconfont/iconfont.woff +0 -0
  23. package/src/assets/iconfont/iconfont.woff2 +0 -0
  24. package/src/components/CanvasArea.vue +218 -193
  25. package/src/components/ChartTypeSelector.vue +509 -148
  26. package/src/components/Header.vue +327 -44
  27. package/src/components/PresentationGroupChild.vue +44 -13
  28. package/src/components/PresentationMode.vue +84 -33
  29. package/src/components/QuickBorderPopover.vue +51 -11
  30. package/src/components/QuickColorPopover.vue +65 -21
  31. package/src/components/SidebarLeft.vue +286 -107
  32. package/src/components/SidebarRight.vue +45 -38
  33. package/src/components/TemplateGallery.vue +1110 -1237
  34. package/src/components/ToastNotification.vue +2 -2
  35. package/src/components/Toolbar.vue +825 -733
  36. package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
  37. package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
  38. package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
  39. package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
  40. package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
  41. package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
  42. package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
  43. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
  44. package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
  45. package/src/components/sidebar/panels/GradientEditor.vue +671 -316
  46. package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
  47. package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
  48. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  49. package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
  50. package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
  51. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
  52. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  53. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
  54. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
  55. package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
  56. package/src/composables/canvas/useCanvasText.ts +1 -1
  57. package/src/composables/canvas/useCanvasTransform.ts +44 -0
  58. package/src/composables/presetThemes.ts +43 -44
  59. package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
  60. package/src/composables/sidebar/useSidebarDesign.ts +30 -11
  61. package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
  62. package/src/composables/sidebar/useSidebarImage.ts +10 -2
  63. package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
  64. package/src/composables/useSlides.ts +331 -153
  65. package/src/main.ts +2 -0
  66. package/src/style.css +926 -359
  67. package/src/utils/canvas/elementStyles.ts +42 -1
@@ -91,6 +91,20 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
91
91
  function handleGlobalKeyDown(e: KeyboardEvent) {
92
92
  if (e.defaultPrevented) return
93
93
 
94
+ // 如果焦点在输入框、文本域或富文本编辑区(无论是在 PPT 内部还是在外部宿主中),绝不拦截原生输入行为
95
+ const activeEl = document.activeElement as HTMLElement | null
96
+ const target = (e.target || activeEl) as HTMLElement | null
97
+ if (
98
+ target &&
99
+ (target.tagName === 'INPUT' ||
100
+ target.tagName === 'TEXTAREA' ||
101
+ target.isContentEditable ||
102
+ target.getAttribute('contenteditable') === 'true' ||
103
+ target.closest?.('input, textarea, [contenteditable="true"]'))
104
+ ) {
105
+ return
106
+ }
107
+
94
108
  if (isReadonly?.value) {
95
109
  if (e.key === 'F5') {
96
110
  e.preventDefault()
@@ -270,13 +284,13 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
270
284
  if (isEditingText.value) return
271
285
 
272
286
  // Prevent deleting slides or moving elements when editing other standard input elements
273
- const activeEl = document.activeElement as HTMLElement | null
287
+ const currentActiveEl = document.activeElement as HTMLElement | null
274
288
  if (
275
- activeEl &&
276
- (activeEl.tagName === 'INPUT' ||
277
- activeEl.tagName === 'TEXTAREA' ||
278
- activeEl.isContentEditable ||
279
- activeEl.getAttribute('contenteditable') === 'true')
289
+ currentActiveEl &&
290
+ (currentActiveEl.tagName === 'INPUT' ||
291
+ currentActiveEl.tagName === 'TEXTAREA' ||
292
+ currentActiveEl.isContentEditable ||
293
+ currentActiveEl.getAttribute('contenteditable') === 'true')
280
294
  ) {
281
295
  return
282
296
  }
@@ -385,7 +385,7 @@ export function useCanvasText(options: TextOptions) {
385
385
  if (container) {
386
386
  container.innerHTML = isShape ? (isSvgPathString(templateDefault) ? '' : templateDefault) : templateDefault
387
387
  }
388
- } else if (prevEl.type === 'text') {
388
+ } else if (prevEl.type === 'text' && (!clickedEl || clickedEl.id !== prevEl.id)) {
389
389
  activeSlide.value.elements = activeSlide.value.elements.filter((x: any) => x.id !== prevEl.id)
390
390
  if (selectedElementId.value === prevEl.id) {
391
391
  selectedElementId.value = null
@@ -47,6 +47,7 @@ export interface CanvasTransformOptions {
47
47
  updateLineGeometry?: (el: any, start: [number, number], end: [number, number], absControls?: any, allElements?: any[], parentGroupDim?: { left: number; top: number; width: number; height: number }) => void
48
48
  selectedElementWidth?: Ref<string>
49
49
  selectedElementHeight?: Ref<string>
50
+ activeSlideVersion?: Ref<number>
50
51
  }
51
52
 
52
53
  export function useCanvasTransform(options: CanvasTransformOptions) {
@@ -349,9 +350,31 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
349
350
  if (typeof item.height === 'number') item.height = newH
350
351
  }
351
352
 
353
+ if (item.type === 'shape' && item.pathFormula === 'roundRect') {
354
+ const formulaObj = shapeFormulas()[item.pathFormula]
355
+ if (formulaObj && typeof formulaObj.formula === 'function') {
356
+ const kps = Array.isArray(item.keypoints) && item.keypoints.length > 0
357
+ ? item.keypoints
358
+ : (formulaObj.defaultValue ? [...formulaObj.defaultValue] : [0.125])
359
+ item.viewBox = [newW, newH]
360
+ const newPath = formulaObj.formula(newW, newH, kps)
361
+ item.content = newPath
362
+ item.path = newPath
363
+ }
364
+ }
365
+
352
366
  // Auto-update font scaling for autoFit text boxes
353
367
  updateTextFontScale(item)
354
368
 
369
+ if (selectedElementId.value === item.id) {
370
+ if (options.selectedElementWidth && !isAutoWidthText) {
371
+ options.selectedElementWidth.value = `${newW.toFixed(1)}px`
372
+ }
373
+ if (options.selectedElementHeight && !isAutoHeightText) {
374
+ options.selectedElementHeight.value = `${newH.toFixed(1)}px`
375
+ }
376
+ }
377
+
355
378
  // Real-time table resize handling
356
379
  if (item.type === 'text' && item.content && typeof item.content === 'string' && item.content.includes('<table')) {
357
380
  const canvasDom = document.getElementById(`canvas-el-${item.id}`)
@@ -431,6 +454,12 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
431
454
 
432
455
  for (const state of resizeStates) {
433
456
  const item = state.el
457
+ if (item && selectedElementId.value === item.id) {
458
+ const curW = item.style?.width && item.style.width !== 'auto' ? parseFloat(item.style.width) : (typeof item.width === 'number' ? item.width : 0)
459
+ const curH = item.style?.height && item.style.height !== 'auto' ? parseFloat(item.style.height) : (typeof item.height === 'number' ? item.height : 0)
460
+ if (options.selectedElementWidth && curW > 0) options.selectedElementWidth.value = `${curW}px`
461
+ if (options.selectedElementHeight && curH > 0) options.selectedElementHeight.value = `${curH}px`
462
+ }
434
463
  if (item && item.type === 'text' && item.content && typeof item.content === 'string' && item.content.includes('<table')) {
435
464
  const container = document.getElementById(`editable-${item.id}`)
436
465
  if (container) {
@@ -455,6 +484,7 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
455
484
  if (isCanvasDragging) isCanvasDragging.value = false
456
485
  pauseHistory.value = false
457
486
  addHistorySnapshot()
487
+ if (options.activeSlideVersion) options.activeSlideVersion.value++
458
488
 
459
489
  currentElement.value = null
460
490
  resizeStates = []
@@ -566,6 +596,7 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
566
596
  if (isCanvasDragging) isCanvasDragging.value = false
567
597
  pauseHistory.value = false
568
598
  addHistorySnapshot()
599
+ if (options.activeSlideVersion) options.activeSlideVersion.value++
569
600
  }
570
601
 
571
602
  function startDragKeypoint(e: MouseEvent, el: any, index: number) {
@@ -627,6 +658,9 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
627
658
  const geom = getElementPixels(curEl)
628
659
  const actualW = geom.width || 200
629
660
  const actualH = geom.height || 200
661
+ if (curEl.pathFormula === 'roundRect') {
662
+ curEl.viewBox = [actualW, actualH]
663
+ }
630
664
  const viewBoxW = curEl.viewBox?.[0] || 200
631
665
  const viewBoxH = curEl.viewBox?.[1] || 200
632
666
 
@@ -1030,6 +1064,14 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
1030
1064
  const newPath = formulaObj.formula(viewBoxW, viewBoxH, curEl.keypoints)
1031
1065
  curEl.content = newPath
1032
1066
  curEl.path = newPath
1067
+ if (curEl.pathFormula === 'roundRect') {
1068
+ if (curEl.style && curEl.style.borderRadius !== undefined) {
1069
+ delete curEl.style.borderRadius
1070
+ }
1071
+ if (curEl.radius !== undefined) {
1072
+ delete curEl.radius
1073
+ }
1074
+ }
1033
1075
 
1034
1076
  const parentGroup = findParentGroup(curEl.id)
1035
1077
  if (parentGroup && parentGroup.elements) {
@@ -1057,6 +1099,7 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
1057
1099
  currentElement.value = null
1058
1100
  pauseHistory.value = false
1059
1101
  addHistorySnapshot()
1102
+ if (options.activeSlideVersion) options.activeSlideVersion.value++
1060
1103
  }
1061
1104
 
1062
1105
  function startDragLinePoint(e: MouseEvent, el: any, type: 'start' | 'end') {
@@ -1264,6 +1307,7 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
1264
1307
  }
1265
1308
  pauseHistory.value = false
1266
1309
  addHistorySnapshot()
1310
+ if (options.activeSlideVersion) options.activeSlideVersion.value++
1267
1311
  }
1268
1312
 
1269
1313
  return {
@@ -69,20 +69,20 @@ export interface StyleRecipe {
69
69
  }
70
70
 
71
71
  export const defaultStyleRecipe: StyleRecipe = {
72
- id: 'default-recipe',
73
- name: 'Template PresentationGO',
74
- styleColors: ['#ea580c', '#1e293b', '#475569', '#cbd5e1', '#f97316', '#10b981', '#6366f1', '#0ea5e9'],
72
+ id: 'tech-blue',
73
+ name: '商务科技蓝',
74
+ styleColors: ['#2563eb', '#1d4ed8', '#0284c7', '#0ea5e9', '#38bdf8', '#64748b', '#0f172a', '#6366f1'],
75
75
  styleFonts: {
76
76
  titleFont: 'SourceHanSansCN',
77
77
  bodyFont: 'SourceHanSansCN'
78
78
  },
79
- backgroundColor: '#ffffff',
80
- bulletPointsColor: '#64748b',
81
- linkColor: '#ea580c',
79
+ backgroundColor: '#f8fafc',
80
+ bulletPointsColor: '#3b82f6',
81
+ linkColor: '#2563eb',
82
82
  linkUnderline: true,
83
83
  textStyles: {
84
84
  title: {
85
- color: '#0f172a',
85
+ color: '#1e3a8a',
86
86
  fontSize: 48,
87
87
  fontWeight: 'bold',
88
88
  letterSpacing: 0,
@@ -91,8 +91,8 @@ export const defaultStyleRecipe: StyleRecipe = {
91
91
  uppercase: false
92
92
  },
93
93
  headline: {
94
- color: '#0f172a',
95
- fontSize: 28,
94
+ color: '#2563eb',
95
+ fontSize: 32,
96
96
  fontWeight: 'bold',
97
97
  letterSpacing: 0,
98
98
  lineHeight: 1.3,
@@ -100,8 +100,8 @@ export const defaultStyleRecipe: StyleRecipe = {
100
100
  uppercase: false
101
101
  },
102
102
  subheadline: {
103
- color: '#0f172a',
104
- fontSize: 18,
103
+ color: '#0284c7',
104
+ fontSize: 20,
105
105
  fontWeight: 'normal',
106
106
  letterSpacing: 0,
107
107
  lineHeight: 1.3,
@@ -110,7 +110,7 @@ export const defaultStyleRecipe: StyleRecipe = {
110
110
  },
111
111
  normalText: {
112
112
  color: '#334155',
113
- fontSize: 14,
113
+ fontSize: 16,
114
114
  fontWeight: 'normal',
115
115
  letterSpacing: 0,
116
116
  lineHeight: 1.5,
@@ -119,7 +119,7 @@ export const defaultStyleRecipe: StyleRecipe = {
119
119
  },
120
120
  smallText: {
121
121
  color: '#64748b',
122
- fontSize: 12,
122
+ fontSize: 13,
123
123
  fontWeight: 'normal',
124
124
  letterSpacing: 0,
125
125
  lineHeight: 1.4,
@@ -128,27 +128,26 @@ export const defaultStyleRecipe: StyleRecipe = {
128
128
  }
129
129
  },
130
130
  table: {
131
- headerBg: '#e05638',
131
+ headerBg: '#1e40af',
132
132
  headerTextColor: '#ffffff',
133
- borderColor: '#fecaca',
134
- alternateRowBg: '#fde8e4'
133
+ borderColor: '#cbd5e1',
134
+ alternateRowBg: '#eff6ff'
135
135
  },
136
136
  chart: {
137
- chartColor: '#6366f1',
137
+ chartColor: '#0f172a',
138
138
  paletteName: 'Chart colors - Mono',
139
- palette: ['#6366f1', '#7c3aed', '#93c5fd', '#bfdbfe', '#dbeafe', '#eff6ff', '#f8fafc', '#ffffff'],
139
+ palette: generateMonoPalette('#0f172a', 8),
140
140
  labelColor: '#1e293b',
141
141
  gridColor: '#e2e8f0'
142
142
  },
143
143
  shape: {
144
- fillColor: '#ea580c',
145
- borderColor: '#1e293b',
144
+ fillColor: '#2563eb',
145
+ borderColor: '#1d4ed8',
146
146
  textColor: '#ffffff'
147
147
  }
148
148
  }
149
149
 
150
150
  export const builtinStyleRecipes: StyleRecipe[] = [
151
- JSON.parse(JSON.stringify(defaultStyleRecipe)),
152
151
  {
153
152
  id: 'tech-blue',
154
153
  name: '商务科技蓝',
@@ -168,30 +167,11 @@ export const builtinStyleRecipes: StyleRecipe[] = [
168
167
  chart: { chartColor: '#0f172a', paletteName: 'Chart colors - Mono', palette: generateMonoPalette('#0f172a', 8), labelColor: '#1e293b', gridColor: '#e2e8f0' },
169
168
  shape: { fillColor: '#2563eb', borderColor: '#1d4ed8', textColor: '#ffffff' }
170
169
  },
171
- {
172
- id: 'dark-gold',
173
- name: '高端暗黑金',
174
- styleColors: ['#b45309', '#d97706', '#f59e0b', '#fbbf24', '#78350f', '#451a03', '#0f172a', '#1e293b'],
175
- styleFonts: { titleFont: 'Abhaya Libre', bodyFont: 'Abhaya Libre' },
176
- backgroundColor: '#0f172a',
177
- bulletPointsColor: '#f59e0b',
178
- linkColor: '#fbbf24',
179
- textStyles: {
180
- title: { color: '#fbbf24', fontSize: 48, fontWeight: 'bold', letterSpacing: 0, lineHeight: 1.2, paragraphSpacing: 0, uppercase: false },
181
- headline: { color: '#f59e0b', fontSize: 32, fontWeight: 'bold', letterSpacing: 0, lineHeight: 1.3, paragraphSpacing: 0, uppercase: false },
182
- subheadline: { color: '#d97706', fontSize: 20, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.3, paragraphSpacing: 0, uppercase: false },
183
- normalText: { color: '#e2e8f0', fontSize: 16, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.5, paragraphSpacing: 0, uppercase: false },
184
- smallText: { color: '#94a3b8', fontSize: 13, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.4, paragraphSpacing: 0, uppercase: false }
185
- },
186
- table: { headerBg: '#b45309', headerTextColor: '#ffffff', borderColor: '#334155', alternateRowBg: '#1e293b' },
187
- chart: { chartColor: '#0f172a', paletteName: 'Chart colors - Mono', palette: generateMonoPalette('#0f172a', 8), labelColor: '#f8fafc', gridColor: '#334155' },
188
- shape: { fillColor: '#d97706', borderColor: '#fbbf24', textColor: '#ffffff' }
189
- },
190
170
  {
191
171
  id: 'fresh-green',
192
172
  name: '薄荷清新绿',
193
173
  styleColors: ['#10b981', '#059669', '#047857', '#065f46', '#84cc16', '#06b6d4', '#1f2937', '#a7f3d0'],
194
- styleFonts: { titleFont: 'SourceHanSansCN', bodyFont: 'SourceHanSansCN' },
174
+ styleFonts: { titleFont: '抖音美好体', bodyFont: '抖音美好体' },
195
175
  backgroundColor: '#f0fdf4',
196
176
  bulletPointsColor: '#10b981',
197
177
  linkColor: '#059669',
@@ -210,7 +190,7 @@ export const builtinStyleRecipes: StyleRecipe[] = [
210
190
  id: 'fashion-purple',
211
191
  name: '时尚梦幻紫',
212
192
  styleColors: ['#8b5cf6', '#7c3aed', '#6d28d9', '#5b21b6', '#ec4899', '#06b6d4', '#111827', '#ddd6fe'],
213
- styleFonts: { titleFont: 'Montserrat', bodyFont: 'Montserrat' },
193
+ styleFonts: { titleFont: '字魂扁桃体', bodyFont: '字魂扁桃体' },
214
194
  backgroundColor: '#f5f3ff',
215
195
  bulletPointsColor: '#8b5cf6',
216
196
  linkColor: '#7c3aed',
@@ -225,11 +205,30 @@ export const builtinStyleRecipes: StyleRecipe[] = [
225
205
  chart: { chartColor: '#111827', paletteName: 'Chart colors - Mono', palette: generateMonoPalette('#111827', 8), labelColor: '#4c1d95', gridColor: '#ddd6fe' },
226
206
  shape: { fillColor: '#8b5cf6', borderColor: '#6d28d9', textColor: '#ffffff' }
227
207
  },
208
+ {
209
+ id: 'dark-gold',
210
+ name: '高端暗黑金',
211
+ styleColors: ['#b45309', '#d97706', '#f59e0b', '#fbbf24', '#78350f', '#451a03', '#0f172a', '#1e293b'],
212
+ styleFonts: { titleFont: '字魂扁桃体', bodyFont: '字魂扁桃体' },
213
+ backgroundColor: '#121826',
214
+ bulletPointsColor: '#f59e0b',
215
+ linkColor: '#fbbf24',
216
+ textStyles: {
217
+ title: { color: '#fbbf24', fontSize: 48, fontWeight: 'bold', letterSpacing: 0, lineHeight: 1.2, paragraphSpacing: 0, uppercase: false },
218
+ headline: { color: '#f59e0b', fontSize: 32, fontWeight: 'bold', letterSpacing: 0, lineHeight: 1.3, paragraphSpacing: 0, uppercase: false },
219
+ subheadline: { color: '#d97706', fontSize: 20, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.3, paragraphSpacing: 0, uppercase: false },
220
+ normalText: { color: '#e2e8f0', fontSize: 16, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.5, paragraphSpacing: 0, uppercase: false },
221
+ smallText: { color: '#94a3b8', fontSize: 13, fontWeight: 'normal', letterSpacing: 0, lineHeight: 1.4, paragraphSpacing: 0, uppercase: false }
222
+ },
223
+ table: { headerBg: '#b45309', headerTextColor: '#ffffff', borderColor: '#334155', alternateRowBg: '#1e293b' },
224
+ chart: { chartColor: '#0f172a', paletteName: 'Chart colors - Mono', palette: generateMonoPalette('#0f172a', 8), labelColor: '#f8fafc', gridColor: '#334155' },
225
+ shape: { fillColor: '#d97706', borderColor: '#fbbf24', textColor: '#ffffff' }
226
+ },
228
227
  {
229
228
  id: 'warm-amber',
230
- name: '活力暖阳橙',
229
+ name: '活力阳光橙',
231
230
  styleColors: ['#f97316', '#ea580c', '#c2410c', '#f59e0b', '#ef4444', '#475569', '#1e293b', '#fed7aa'],
232
- styleFonts: { titleFont: 'SourceHanSansCN', bodyFont: 'SourceHanSansCN' },
231
+ styleFonts: { titleFont: '字魂扁桃体', bodyFont: '字魂扁桃体' },
233
232
  backgroundColor: '#fffaf5',
234
233
  bulletPointsColor: '#f97316',
235
234
  linkColor: '#ea580c',
@@ -16,6 +16,7 @@ export function useSidebarAnimation() {
16
16
  addAnimation,
17
17
  deleteAnimation,
18
18
  updateAnimationEffect,
19
+ updateAnimationTargetElement,
19
20
  updateAnimationTrigger,
20
21
  updateAnimationDuration,
21
22
  updateAnimationOrder,
@@ -73,9 +74,39 @@ export function useSidebarAnimation() {
73
74
  return null
74
75
  }
75
76
 
77
+ const allSlideElements = computed(() => {
78
+ if (!activeSlide.value?.elements) return []
79
+ const list: any[] = []
80
+ const collect = (els: any[]) => {
81
+ for (const el of els) {
82
+ list.push(el)
83
+ if (el.type === 'group' && Array.isArray(el.elements)) {
84
+ collect(el.elements)
85
+ }
86
+ }
87
+ }
88
+ collect(activeSlide.value.elements)
89
+ return list
90
+ })
91
+
92
+ function inferAnimationType(effect: string): 'in' | 'out' | 'attention' {
93
+ for (const cat of ENTER_ANIMATIONS) {
94
+ if (cat.children?.some(c => c.value === effect)) return 'in'
95
+ }
96
+ for (const cat of EXIT_ANIMATIONS) {
97
+ if (cat.children?.some(c => c.value === effect)) return 'out'
98
+ }
99
+ for (const cat of ATTENTION_ANIMATIONS) {
100
+ if (cat.children?.some(c => c.value === effect)) return 'attention'
101
+ }
102
+ if (effect.includes('Out')) return 'out'
103
+ if (effect.includes('In')) return 'in'
104
+ return 'in'
105
+ }
106
+
76
107
  function getElTypeName(type: string): string {
77
108
  const types: Record<string, string> = {
78
- text: '文本框',
109
+ text: '文本',
79
110
  shape: '形状',
80
111
  image: '图片',
81
112
  video: '视频',
@@ -90,7 +121,7 @@ export function useSidebarAnimation() {
90
121
  if (!activeSlide.value) return ''
91
122
  const el = findElementInSlide(elId)
92
123
  if (!el) return ''
93
- const sameTypeEls = activeSlide.value.elements.filter((e: any) => e.type === el.type)
124
+ const sameTypeEls = allSlideElements.value.filter((e: any) => e.type === el.type)
94
125
  const idx = sameTypeEls.findIndex((e: any) => e.id === elId)
95
126
  return idx !== -1 ? String(idx + 1) : ''
96
127
  }
@@ -118,7 +149,7 @@ export function useSidebarAnimation() {
118
149
  }
119
150
  if (el.type === 'text') {
120
151
  if (el.name) return el.name
121
- return '文本框'
152
+ return '文本'
122
153
  }
123
154
  return getElTypeName(el.type)
124
155
  }
@@ -133,41 +164,59 @@ export function useSidebarAnimation() {
133
164
  function getDirectionConfig(effect: string, type: 'in' | 'out' | 'attention'): DirectionOption[] {
134
165
  if (type === 'attention') return []
135
166
  let animationFamily = ''
136
- if (effect.includes('bounce')) animationFamily = 'bounce'
167
+ if (effect.includes('fade')) animationFamily = 'fade'
168
+ else if (effect.includes('bounce')) animationFamily = 'bounce'
137
169
  else if (effect.includes('back')) animationFamily = 'back'
138
170
  else if (effect.includes('slide')) animationFamily = 'slide'
171
+ else if (effect.includes('zoom')) animationFamily = 'zoom'
139
172
  else if (effect.includes('lightSpeed')) animationFamily = 'lightSpeed'
140
173
  else if (effect.includes('flip')) animationFamily = 'flip'
141
174
 
142
175
  if (!animationFamily) return []
176
+ if (animationFamily === 'fade') {
177
+ return [
178
+ { label: '从底部', value: 'up', effectValue: type === 'in' ? 'fadeInUp' : 'fadeOutUp' },
179
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'fadeInLeft' : 'fadeOutLeft' },
180
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'fadeInRight' : 'fadeOutRight' },
181
+ { label: '从顶部', value: 'down', effectValue: type === 'in' ? 'fadeInDown' : 'fadeOutDown' },
182
+ ]
183
+ }
143
184
  if (animationFamily === 'bounce') {
144
185
  return [
145
- { label: '自底部', value: 'up', effectValue: type === 'in' ? 'bounceInUp' : 'bounceOutUp' },
146
- { label: '自左侧', value: 'left', effectValue: type === 'in' ? 'bounceInLeft' : 'bounceOutLeft' },
147
- { label: '自右侧', value: 'right', effectValue: type === 'in' ? 'bounceInRight' : 'bounceOutRight' },
148
- { label: '自顶部', value: 'down', effectValue: type === 'in' ? 'bounceInDown' : 'bounceOutDown' },
186
+ { label: '从底部', value: 'up', effectValue: type === 'in' ? 'bounceInUp' : 'bounceOutUp' },
187
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'bounceInLeft' : 'bounceOutLeft' },
188
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'bounceInRight' : 'bounceOutRight' },
189
+ { label: '从顶部', value: 'down', effectValue: type === 'in' ? 'bounceInDown' : 'bounceOutDown' },
149
190
  ]
150
191
  }
151
192
  if (animationFamily === 'back') {
152
193
  return [
153
- { label: '自底部', value: 'up', effectValue: type === 'in' ? 'backInUp' : 'backOutUp' },
154
- { label: '自左侧', value: 'left', effectValue: type === 'in' ? 'backInLeft' : 'backOutLeft' },
155
- { label: '自右侧', value: 'right', effectValue: type === 'in' ? 'backInRight' : 'backOutRight' },
156
- { label: '自顶部', value: 'down', effectValue: type === 'in' ? 'backInDown' : 'backOutDown' },
194
+ { label: '从底部', value: 'up', effectValue: type === 'in' ? 'backInUp' : 'backOutUp' },
195
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'backInLeft' : 'backOutLeft' },
196
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'backInRight' : 'backOutRight' },
197
+ { label: '从顶部', value: 'down', effectValue: type === 'in' ? 'backInDown' : 'backOutDown' },
157
198
  ]
158
199
  }
159
200
  if (animationFamily === 'slide') {
160
201
  return [
161
- { label: '自底部', value: 'up', effectValue: type === 'in' ? 'slideInUp' : 'slideOutUp' },
162
- { label: '自左侧', value: 'left', effectValue: type === 'in' ? 'slideInLeft' : 'slideOutLeft' },
163
- { label: '自右侧', value: 'right', effectValue: type === 'in' ? 'slideInRight' : 'slideOutRight' },
164
- { label: '自顶部', value: 'down', effectValue: type === 'in' ? 'slideInDown' : 'slideOutDown' },
202
+ { label: '从底部', value: 'up', effectValue: type === 'in' ? 'slideInUp' : 'slideOutUp' },
203
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'slideInLeft' : 'slideOutLeft' },
204
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'slideInRight' : 'slideOutRight' },
205
+ { label: '从顶部', value: 'down', effectValue: type === 'in' ? 'slideInDown' : 'slideOutDown' },
206
+ ]
207
+ }
208
+ if (animationFamily === 'zoom') {
209
+ return [
210
+ { label: '从底部', value: 'up', effectValue: type === 'in' ? 'zoomInUp' : 'zoomOutUp' },
211
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'zoomInLeft' : 'zoomOutLeft' },
212
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'zoomInRight' : 'zoomOutRight' },
213
+ { label: '从顶部', value: 'down', effectValue: type === 'in' ? 'zoomInDown' : 'zoomOutDown' },
165
214
  ]
166
215
  }
167
216
  if (animationFamily === 'lightSpeed') {
168
217
  return [
169
- { label: '自右侧', value: 'right', effectValue: type === 'in' ? 'lightSpeedInRight' : 'lightSpeedOutRight' },
170
- { label: '自左侧', value: 'left', effectValue: type === 'in' ? 'lightSpeedInLeft' : 'lightSpeedOutLeft' },
218
+ { label: '从右侧', value: 'right', effectValue: type === 'in' ? 'lightSpeedInRight' : 'lightSpeedOutRight' },
219
+ { label: '从左侧', value: 'left', effectValue: type === 'in' ? 'lightSpeedInLeft' : 'lightSpeedOutLeft' },
171
220
  ]
172
221
  }
173
222
  if (animationFamily === 'flip') {
@@ -183,7 +232,7 @@ export function useSidebarAnimation() {
183
232
  if (type === 'attention') return effect
184
233
  if (type === 'in') {
185
234
  if (effect.includes('bounce')) return 'bounceIn'
186
- if (effect.includes('fadeIn') || effect.includes('fadeOut')) return 'fadeIn'
235
+ if (effect.includes('fade')) return 'fadeIn'
187
236
  if (effect.includes('zoom')) return 'zoomIn'
188
237
  if (effect.includes('back')) return 'backInUp'
189
238
  if (effect.includes('slide')) return 'slideInUp'
@@ -192,7 +241,7 @@ export function useSidebarAnimation() {
192
241
  if (effect.includes('flip')) return 'flipInX'
193
242
  } else if (type === 'out') {
194
243
  if (effect.includes('bounce')) return 'bounceOut'
195
- if (effect.includes('fadeIn') || effect.includes('fadeOut')) return 'fadeOut'
244
+ if (effect.includes('fade')) return 'fadeOut'
196
245
  if (effect.includes('zoom')) return 'zoomOut'
197
246
  if (effect.includes('back')) return 'backOutDown'
198
247
  if (effect.includes('slide')) return 'slideOutDown'
@@ -206,6 +255,9 @@ export function useSidebarAnimation() {
206
255
  function getCurrentDirectionValue(anim: any): string {
207
256
  const configs = getDirectionConfig(anim.effect, anim.type)
208
257
  if (configs.length === 0) return 'none'
258
+ if (anim.direction && configs.some(c => c.value === anim.direction)) {
259
+ return anim.direction
260
+ }
209
261
  const match = configs.find(c => c.effectValue === anim.effect)
210
262
  return match ? match.value : configs[0].value
211
263
  }
@@ -216,16 +268,21 @@ export function useSidebarAnimation() {
216
268
  if (match) {
217
269
  updateAnimationEffect(anim.id, anim.type, match.effectValue)
218
270
  anim.direction = match.value
271
+ previewAnimationInCanvas(anim.elId, match.effectValue, anim.duration || 1000)
219
272
  addHistorySnapshot()
220
273
  }
221
274
  }
222
275
 
223
276
  const allEffects = computed(() => {
224
277
  const list: { label: string; value: string }[] = []
278
+ const seenLabels = new Set<string>()
225
279
  const collect = (arr: any[]) => {
226
280
  for (const group of arr) {
227
281
  for (const item of group.children) {
228
- list.push({ label: item.name, value: item.value })
282
+ if (!seenLabels.has(item.name)) {
283
+ seenLabels.add(item.name)
284
+ list.push({ label: item.name, value: item.value })
285
+ }
229
286
  }
230
287
  }
231
288
  }
@@ -237,7 +294,7 @@ export function useSidebarAnimation() {
237
294
 
238
295
  function previewAnimationInCanvas(elId: string, effect: string, duration: number) {
239
296
  if (!activeSlide.value) return
240
- const targetEl = activeSlide.value.elements.find((e: any) => e.id === elId)
297
+ const targetEl = findElementInSlide(elId)
241
298
  const groupId = targetEl?.groupId
242
299
 
243
300
  const targetIds = groupId
@@ -245,7 +302,8 @@ export function useSidebarAnimation() {
245
302
  : [elId]
246
303
 
247
304
  for (const id of targetIds) {
248
- const elRef = document.getElementById(`canvas-el-${id}`) || document.getElementById(`canvas-content-${id}`)
305
+ // 优先在真实内容容器 canvas-content-${id} 上播放动画,避免将选中边框和控制点手柄整体晃动
306
+ const elRef = document.getElementById(`canvas-content-${id}`) || document.getElementById(`canvas-el-${id}`)
249
307
  if (elRef) {
250
308
  const animationName = `animate__${effect}`
251
309
  elRef.style.setProperty('--animate-duration', `${duration}ms`)
@@ -260,8 +318,9 @@ export function useSidebarAnimation() {
260
318
  }
261
319
  }
262
320
 
263
- function handleSelectAnimation(effect: string) {
264
- addAnimation(activeAnimTab.value as any, effect)
321
+ function handleSelectAnimation(effect: string, type?: 'in' | 'out' | 'attention') {
322
+ const animType = type || (activeAnimTab.value as any)
323
+ addAnimation(animType, effect)
265
324
  animationPopoverVisible.value = false
266
325
  addHistorySnapshot()
267
326
  setTimeout(() => {
@@ -271,6 +330,19 @@ export function useSidebarAnimation() {
271
330
  }, 50)
272
331
  }
273
332
 
333
+ function handleUpdateAnimationTargetElement(animId: string, newElId: string) {
334
+ updateAnimationTargetElement(animId, newElId)
335
+ if (selectedElementId) {
336
+ selectedElementId.value = newElId
337
+ }
338
+ const anim = activeSlide.value?.animations?.find((a: any) => a.id === animId)
339
+ if (anim) {
340
+ setTimeout(() => {
341
+ previewAnimationInCanvas(newElId, anim.effect, anim.duration || 1000)
342
+ }, 50)
343
+ }
344
+ }
345
+
274
346
  function toggleAnimationCard(animId: string, elId?: string) {
275
347
  if (elId && selectedElementId) {
276
348
  selectedElementId.value = elId
@@ -292,7 +364,9 @@ export function useSidebarAnimation() {
292
364
  dragOverIndex,
293
365
  activeAnimationsList,
294
366
  allEffects,
367
+ allSlideElements,
295
368
  findElementInSlide,
369
+ inferAnimationType,
296
370
  getElTypeName,
297
371
  getElementIndexName,
298
372
  getAnimationStepIndex,
@@ -307,10 +381,12 @@ export function useSidebarAnimation() {
307
381
  handleDragEnd,
308
382
  handleDrop,
309
383
  handleSelectAnimation,
384
+ handleUpdateAnimationTargetElement,
310
385
  previewAnimationInCanvas,
311
386
  toggleAnimationCard,
312
387
  deleteAnimation,
313
388
  updateAnimationEffect,
389
+ updateAnimationTargetElement,
314
390
  updateAnimationTrigger,
315
391
  updateAnimationDuration,
316
392
  addHistorySnapshot
@@ -17,16 +17,25 @@ export function useSidebarDesign() {
17
17
  id: master.id,
18
18
  name: master.name || '默认母版',
19
19
  background: master.background || '#ffffff',
20
- layouts: (master.layouts || []).map(layout => ({
21
- id: layout.id,
22
- name: layout.name || '自定义版式',
23
- background: layout.background || master.background || '#ffffff',
24
- // 该合并列表仅供缩略图渲染(弹层/设计面板)。导入版式的占位符按分流契约
25
- // 只进 placeholders,若不并入,占位符型版式(标题/两栏等)缩略图会整片空白。
26
- // 应用版式(addSlideWithLayout/applyLayoutToCurrentSlide)走 findMasterLayout
27
- // 读 slideMasters 原始数据,不受此处影响。
28
- elements: [...(master.elements || []), ...(layout.elements || []), ...(layout.placeholders || [])]
29
- }))
20
+ layouts: (master.layouts || []).map(layout => {
21
+ const extraPlaceholders = (layout.type === 'custom' && Array.isArray(layout.elements) && layout.elements.length > 0)
22
+ ? []
23
+ : (layout.placeholders || [])
24
+ const rawList = [...(master.elements || []), ...(layout.elements || []), ...extraPlaceholders]
25
+ const seenIds = new Set<string>()
26
+ const deduped = rawList.filter((el: any) => {
27
+ if (!el) return false
28
+ if (el.id && seenIds.has(el.id)) return false
29
+ if (el.id) seenIds.add(el.id)
30
+ return true
31
+ })
32
+ return {
33
+ id: layout.id,
34
+ name: layout.name || '自定义版式',
35
+ background: layout.background || master.background || '#ffffff',
36
+ elements: deduped
37
+ }
38
+ })
30
39
  }))
31
40
  })
32
41
 
@@ -124,7 +133,17 @@ export function useSidebarDesign() {
124
133
  }
125
134
 
126
135
  const parentMaster = (slideMasters.value || []).find((m: any) => (m.layouts || []).some((l: any) => l.id === masterLayout.id))
127
- const layoutElements = [...(masterLayout.elements || []), ...(masterLayout.placeholders || [])]
136
+ const extraPlaceholders = (masterLayout.type === 'custom' && Array.isArray(masterLayout.elements) && masterLayout.elements.length > 0)
137
+ ? []
138
+ : (masterLayout.placeholders || [])
139
+ const rawLayoutElements = [...(masterLayout.elements || []), ...extraPlaceholders]
140
+ const seenIds = new Set<string>()
141
+ const layoutElements = rawLayoutElements.filter((el: any) => {
142
+ if (!el) return false
143
+ if (el.id && seenIds.has(el.id)) return false
144
+ if (el.id) seenIds.add(el.id)
145
+ return true
146
+ })
128
147
  const newElements = layoutElements.map((el: any) => {
129
148
  const cloned = JSON.parse(JSON.stringify(el))
130
149
  const isPh = isLayoutPlaceholderElement(cloned)