jvs-ppt-ui 0.0.15 → 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 (27) hide show
  1. package/dist/lib/jvs-ppt-ui.es.js +20301 -19823
  2. package/dist/lib/jvs-ppt-ui.umd.js +99 -99
  3. package/dist/lib/style.css +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CanvasArea.vue +14 -115
  6. package/src/components/ChartTypeSelector.vue +34 -30
  7. package/src/components/Header.vue +40 -5
  8. package/src/components/PresentationGroupChild.vue +44 -13
  9. package/src/components/PresentationMode.vue +84 -33
  10. package/src/components/SidebarLeft.vue +96 -18
  11. package/src/components/SidebarRight.vue +6 -3
  12. package/src/components/Toolbar.vue +1 -1
  13. package/src/components/canvas/elements/CanvasShapeElement.vue +91 -34
  14. package/src/components/sidebar/common/SidebarBorderAccordion.vue +2 -2
  15. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +2 -2
  16. package/src/components/sidebar/common/SidebarShadowAccordion.vue +2 -2
  17. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  18. package/src/components/sidebar/panels/SidebarDesignPanel.vue +170 -9
  19. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +176 -16
  20. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  21. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +2 -2
  22. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +30 -19
  23. package/src/composables/canvas/useCanvasTransform.ts +24 -0
  24. package/src/composables/sidebar/useSidebarShapeFormat.ts +248 -17
  25. package/src/composables/useSlides.ts +12 -0
  26. package/src/style.css +4 -2
  27. package/src/utils/canvas/elementStyles.ts +22 -0
@@ -2,6 +2,8 @@
2
2
  import { computed, ref, watch, onBeforeUnmount, nextTick } from 'vue'
3
3
  import {
4
4
  getElementPattern,
5
+ isElementPatternTile,
6
+ getElementPatternTileSize,
5
7
  getElementGradientConfig,
6
8
  getElementGradientCss,
7
9
  getSvgGradientCoords,
@@ -24,7 +26,8 @@ import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../
24
26
 
25
27
  const { shapeFormatPainter, isReadonly } = useSlides()
26
28
  const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
27
- const isTextSelectable = computed(() => (props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value)
29
+ const svgIdKey = computed(() => props.isReflection ? `${props.element.id}-refl` : `${props.element.id}`)
30
+ const isTextSelectable = computed(() => !props.isReflection && ((props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value))
28
31
 
29
32
  function getEffectiveBorderWidth(el: any): number {
30
33
  if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 0
@@ -35,6 +38,23 @@ function getEffectiveBorderWidth(el: any): number {
35
38
  return 0
36
39
  }
37
40
 
41
+ function getShapePatternTransform(el: any, isTriangle = false): string | undefined {
42
+ if (!el || !isElementPatternTile(el)) return undefined
43
+ const { width, height } = getElementWidthHeight(el)
44
+ if (isTriangle) {
45
+ const sx = width > 0 ? 100 / width : 1
46
+ const sy = height > 0 ? 100 / height : 1
47
+ return `scale(${sx}, ${sy})`
48
+ }
49
+ const vbW = el.viewBox ? el.viewBox[0] : (width || 200)
50
+ const vbH = el.viewBox ? el.viewBox[1] : (height || 200)
51
+ const hasBorder = hasElementBorder(el)
52
+ const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
53
+ const effW = bw > 0 ? Math.max(1, width - bw) : Math.max(1, width)
54
+ const effH = bw > 0 ? Math.max(1, height - bw) : Math.max(1, height)
55
+ return `scale(${vbW / effW}, ${vbH / effH})`
56
+ }
57
+
38
58
  function getShapeSvgTransform(el: any): string | undefined {
39
59
  if (!el) return undefined
40
60
  const { width, height } = getElementWidthHeight(el)
@@ -65,7 +85,7 @@ function getShapeSvgTransform(el: any): string | undefined {
65
85
  function getEffectiveBorderColor(el: any, fallback = '#10b981'): string {
66
86
  if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 'transparent'
67
87
  if (el.style?.enableBorder === false) return 'transparent'
68
- if (hasElementBorderGradient(el)) return `url(#shape-border-gradient-${el.id})`
88
+ if (hasElementBorderGradient(el)) return `url(#shape-border-gradient-${svgIdKey.value})`
69
89
  return el.style?.borderColor || getElementBorderColor(el) || fallback
70
90
  }
71
91
 
@@ -102,22 +122,32 @@ function isRectShape(el: any): boolean {
102
122
 
103
123
  function getRectRadiusInVb(el: any): { rx: number; ry: number } {
104
124
  if (!el) return { rx: 0, ry: 0 }
125
+ const { width, height } = getElementWidthHeight(el)
105
126
  let r = 0
127
+ const isRoundRect = el.pathFormula === 'roundRect' || el.shapType === 'roundRect' || el.title === '圆角矩形'
106
128
  const rVal = el.style?.borderRadius !== undefined && el.style?.borderRadius !== null && el.style?.borderRadius !== ''
107
129
  ? el.style.borderRadius
108
130
  : el.radius
109
131
 
110
- if (rVal !== undefined && rVal !== null && rVal !== '') {
132
+ if (isRoundRect && Array.isArray(el.keypoints) && el.keypoints[0] !== undefined && el.keypoints[0] !== null) {
133
+ const kp = Number(el.keypoints[0])
134
+ if (!isNaN(kp)) {
135
+ r = Math.min(width, height) * Math.max(0, Math.min(0.5, kp))
136
+ }
137
+ } else if (isRoundRect && el.keypoints && typeof el.keypoints === 'object' && !Array.isArray(el.keypoints) && (el.keypoints as any).adj !== undefined) {
138
+ const adj = Number((el.keypoints as any).adj)
139
+ if (!isNaN(adj)) {
140
+ r = Math.min(width, height) * Math.max(0, Math.min(0.5, adj * 0.5))
141
+ }
142
+ } else if (rVal !== undefined && rVal !== null && rVal !== '') {
111
143
  const parsed = parseFloat(String(rVal))
112
144
  if (!isNaN(parsed)) r = parsed
113
- } else if (el.pathFormula === 'roundRect' || el.shapType === 'roundRect') {
114
- const { width, height } = getElementWidthHeight(el)
115
- r = Math.min(width, height) * 0.15
145
+ } else if (isRoundRect) {
146
+ r = Math.min(width, height) * 0.125
116
147
  }
117
148
 
118
149
  if (r <= 0) return { rx: 0, ry: 0 }
119
150
 
120
- const { width, height } = getElementWidthHeight(el)
121
151
  const { w: vbW, h: vbH } = getShapeVb(el)
122
152
 
123
153
  const maxR = Math.min(width, height) / 2
@@ -146,6 +176,7 @@ const props = defineProps<{
146
176
  getElementTextShadow?: (el: any) => string
147
177
  getReflectionLeadingCompensation?: (el: any) => number
148
178
  isImagePlaceholder?: (el: any) => boolean
179
+ isReflection?: boolean
149
180
  // 直接父盒子(组合)的像素尺寸,用于把 % 样式的宽高解析成 px。
150
181
  // 由 CanvasGroupElement 传入;顶层元素不传,走 px 样式分支。
151
182
  parentWidthPx?: number
@@ -345,9 +376,9 @@ function formatShapeHtml(el: any): string {
345
376
  height: '100%',
346
377
  backgroundColor: getElementPattern(element) ? 'transparent' : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#3b82f6')),
347
378
  backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
348
- backgroundSize: getElementPattern(element) ? 'cover' : undefined,
349
- backgroundPosition: getElementPattern(element) ? 'center center' : undefined,
350
- backgroundRepeat: getElementPattern(element) ? 'no-repeat' : undefined,
379
+ backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
380
+ backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
381
+ backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
351
382
  borderRadius: '50%',
352
383
  borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
353
384
  ? '0px'
@@ -375,7 +406,7 @@ function formatShapeHtml(el: any): string {
375
406
  <defs v-if="getElementBorderGradientConfig(element)">
376
407
  <radialGradient
377
408
  v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
378
- :id="`shape-border-gradient-${element.id}`"
409
+ :id="`shape-border-gradient-${svgIdKey}`"
379
410
  :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
380
411
  :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
381
412
  :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -392,7 +423,7 @@ function formatShapeHtml(el: any): string {
392
423
  </radialGradient>
393
424
  <linearGradient
394
425
  v-else
395
- :id="`shape-border-gradient-${element.id}`"
426
+ :id="`shape-border-gradient-${svgIdKey}`"
396
427
  :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
397
428
  :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
398
429
  :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
@@ -430,13 +461,26 @@ function formatShapeHtml(el: any): string {
430
461
  :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
431
462
  >
432
463
  <defs>
433
- <pattern v-if="getElementPattern(element)" :id="`shape-pattern-${element.id}`" width="100%" height="100%" patternContentUnits="objectBoundingBox">
434
- <image :href="getElementPattern(element)" width="1" height="1" preserveAspectRatio="none" />
464
+ <pattern
465
+ v-if="getElementPattern(element)"
466
+ :id="`shape-pattern-${svgIdKey}`"
467
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
468
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
469
+ :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
470
+ :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
471
+ :patternTransform="getShapePatternTransform(element, true)"
472
+ >
473
+ <image
474
+ :href="getElementPattern(element)"
475
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
476
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
477
+ preserveAspectRatio="none"
478
+ />
435
479
  </pattern>
436
480
  <template v-if="getElementGradientConfig(element)">
437
481
  <radialGradient
438
482
  v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
439
- :id="`shape-gradient-${element.id}`"
483
+ :id="`shape-gradient-${svgIdKey}`"
440
484
  :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
441
485
  :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
442
486
  :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -453,7 +497,7 @@ function formatShapeHtml(el: any): string {
453
497
  </radialGradient>
454
498
  <linearGradient
455
499
  v-else
456
- :id="`shape-gradient-${element.id}`"
500
+ :id="`shape-gradient-${svgIdKey}`"
457
501
  :x1="getSvgGradientCoords(element).x1"
458
502
  :y1="getSvgGradientCoords(element).y1"
459
503
  :x2="getSvgGradientCoords(element).x2"
@@ -471,7 +515,7 @@ function formatShapeHtml(el: any): string {
471
515
  <template v-if="getElementBorderGradientConfig(element)">
472
516
  <radialGradient
473
517
  v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
474
- :id="`shape-border-gradient-${element.id}`"
518
+ :id="`shape-border-gradient-${svgIdKey}`"
475
519
  :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
476
520
  :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
477
521
  :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -488,7 +532,7 @@ function formatShapeHtml(el: any): string {
488
532
  </radialGradient>
489
533
  <linearGradient
490
534
  v-else
491
- :id="`shape-border-gradient-${element.id}`"
535
+ :id="`shape-border-gradient-${svgIdKey}`"
492
536
  :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
493
537
  :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
494
538
  :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
@@ -506,7 +550,7 @@ function formatShapeHtml(el: any): string {
506
550
  </defs>
507
551
  <polygon
508
552
  points="50,0 0,100 100,100"
509
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${element.id})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${element.id})` : (element.style?.fill !== undefined ? element.style.fill : '#f59e0b'))) : 'none'"
553
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (element.style?.fill !== undefined ? element.style.fill : '#f59e0b'))) : 'none'"
510
554
  :stroke="getEffectiveBorderColor(element, '#d97706')"
511
555
  :stroke-width="getEffectiveBorderWidth(element)"
512
556
  :stroke-dasharray="element.style?.borderStyle === 'dashed' ? '5 5' : ''"
@@ -525,13 +569,26 @@ function formatShapeHtml(el: any): string {
525
569
  :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
526
570
  >
527
571
  <defs>
528
- <pattern v-if="getElementPattern(element)" :id="`shape-pattern-${element.id}`" width="100%" height="100%" patternContentUnits="objectBoundingBox">
529
- <image :href="getElementPattern(element)" width="1" height="1" preserveAspectRatio="none" />
572
+ <pattern
573
+ v-if="getElementPattern(element)"
574
+ :id="`shape-pattern-${svgIdKey}`"
575
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
576
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
577
+ :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
578
+ :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
579
+ :patternTransform="getShapePatternTransform(element, false)"
580
+ >
581
+ <image
582
+ :href="getElementPattern(element)"
583
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
584
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
585
+ preserveAspectRatio="none"
586
+ />
530
587
  </pattern>
531
588
  <template v-if="getElementGradientConfig(element)">
532
589
  <radialGradient
533
590
  v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
534
- :id="`shape-gradient-${element.id}`"
591
+ :id="`shape-gradient-${svgIdKey}`"
535
592
  :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
536
593
  :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
537
594
  :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -548,7 +605,7 @@ function formatShapeHtml(el: any): string {
548
605
  </radialGradient>
549
606
  <linearGradient
550
607
  v-else
551
- :id="`shape-gradient-${element.id}`"
608
+ :id="`shape-gradient-${svgIdKey}`"
552
609
  :x1="getSvgGradientCoords(element).x1"
553
610
  :y1="getSvgGradientCoords(element).y1"
554
611
  :x2="getSvgGradientCoords(element).x2"
@@ -566,7 +623,7 @@ function formatShapeHtml(el: any): string {
566
623
  <template v-if="getElementBorderGradientConfig(element)">
567
624
  <radialGradient
568
625
  v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
569
- :id="`shape-border-gradient-${element.id}`"
626
+ :id="`shape-border-gradient-${svgIdKey}`"
570
627
  :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
571
628
  :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
572
629
  :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -583,7 +640,7 @@ function formatShapeHtml(el: any): string {
583
640
  </radialGradient>
584
641
  <linearGradient
585
642
  v-else
586
- :id="`shape-border-gradient-${element.id}`"
643
+ :id="`shape-border-gradient-${svgIdKey}`"
587
644
  :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
588
645
  :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
589
646
  :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
@@ -611,7 +668,7 @@ function formatShapeHtml(el: any): string {
611
668
  :rx="getRectRadiusInVb(element).rx"
612
669
  :ry="getRectRadiusInVb(element).ry"
613
670
  vector-effect="non-scaling-stroke"
614
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${element.id})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${element.id})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
671
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
615
672
  :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
616
673
  :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
617
674
  :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
@@ -621,7 +678,7 @@ function formatShapeHtml(el: any): string {
621
678
  v-else
622
679
  vector-effect="non-scaling-stroke"
623
680
  :d="element.path || element.content"
624
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${element.id})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${element.id})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
681
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
625
682
  :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
626
683
  :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
627
684
  :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
@@ -649,9 +706,9 @@ function formatShapeHtml(el: any): string {
649
706
  height: '100%',
650
707
  backgroundColor: getElementPattern(element) ? 'transparent' : ((isImagePlaceholder && isImagePlaceholder(element) && !getElementGradientCss(element)) ? getImagePlaceholderFill(element) : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#10b981'))),
651
708
  backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
652
- backgroundSize: getElementPattern(element) ? 'cover' : undefined,
653
- backgroundPosition: getElementPattern(element) ? 'center center' : undefined,
654
- backgroundRepeat: getElementPattern(element) ? 'no-repeat' : undefined,
709
+ backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
710
+ backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
711
+ backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
655
712
  borderRadius: (isImagePlaceholder && isImagePlaceholder(element)) ? getImagePlaceholderRadius(element) : (element.style?.borderRadius !== undefined ? (typeof element.style.borderRadius === 'number' ? `${element.style.borderRadius}px` : element.style.borderRadius) : (element.radius ? `${element.radius}px` : '0px')),
656
713
  borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
657
714
  ? '0px'
@@ -677,7 +734,7 @@ function formatShapeHtml(el: any): string {
677
734
  <defs v-if="getElementBorderGradientConfig(element)">
678
735
  <radialGradient
679
736
  v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
680
- :id="`shape-border-gradient-${element.id}`"
737
+ :id="`shape-border-gradient-${svgIdKey}`"
681
738
  :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
682
739
  :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
683
740
  :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
@@ -694,7 +751,7 @@ function formatShapeHtml(el: any): string {
694
751
  </radialGradient>
695
752
  <linearGradient
696
753
  v-else
697
- :id="`shape-border-gradient-${element.id}`"
754
+ :id="`shape-border-gradient-${svgIdKey}`"
698
755
  :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
699
756
  :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
700
757
  :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
@@ -759,7 +816,7 @@ function formatShapeHtml(el: any): string {
759
816
  }">
760
817
  <div
761
818
  ref="shapeEditableRef"
762
- :id="`editable-${element.id}`"
819
+ :id="isReflection ? undefined : `editable-${element.id}`"
763
820
  :contenteditable="isTextSelectable ? 'true' : 'false'"
764
821
  @mousedown="isTextSelectable ? $event.stopPropagation() : null"
765
822
  @click="isTextSelectable ? $event.stopPropagation() : null"
@@ -784,7 +841,7 @@ function formatShapeHtml(el: any): string {
784
841
  boxSizing: 'content-box',
785
842
  overflow: 'wrap',
786
843
  wordBreak: 'break-word',
787
- pointerEvents: isTextSelectable ? 'auto' : (getRawText(element).trim() ? 'auto' : 'none'),
844
+ pointerEvents: isReflection ? 'none' : (isTextSelectable ? 'auto' : (getRawText(element).trim() ? 'auto' : 'none')),
788
845
  cursor: isTextSelectable ? 'text' : undefined,
789
846
  userSelect: isTextSelectable ? 'text' : 'none',
790
847
  color: element.style?.textColor || element.style?.color || element.textColor || ((isImagePlaceholder && isImagePlaceholder(element)) ? '#6366f1' : (element.style?.fill === '#ffffff' || element.style?.fill === '#FFFFFF' ? '#475569' : (themeRecipe?.shape?.textColor || '#ffffff'))),
@@ -250,8 +250,8 @@ function onBorderWidthBlur(e: Event) {
250
250
  }
251
251
 
252
252
  .shape-accordion-header-label {
253
- font-size: 13px;
254
- font-weight: 600;
253
+ font-size: 14px;
254
+ font-weight: 400;
255
255
  color: #1e293b;
256
256
  display: flex;
257
257
  align-items: center;
@@ -297,8 +297,8 @@ function onOpacityBlur(e: Event) {
297
297
  }
298
298
 
299
299
  .shape-accordion-header-label {
300
- font-size: 13px;
301
- font-weight: 600;
300
+ font-size: 14px;
301
+ font-weight: 400;
302
302
  color: #1e293b;
303
303
  display: flex;
304
304
  align-items: center;
@@ -481,8 +481,8 @@ function onAlphaBlur(e: Event) {
481
481
  }
482
482
 
483
483
  .shape-accordion-header-label {
484
- font-size: 13px;
485
- font-weight: 600;
484
+ font-size: 14px;
485
+ font-weight: 400;
486
486
  color: #1e293b;
487
487
  display: flex;
488
488
  align-items: center;
@@ -229,7 +229,7 @@ function toggleFillSwitch(val: any) {
229
229
  <div class="shape-property-row"
230
230
  style="display: flex; align-items: center; justify-content: space-between; gap: 8px; height: auto;">
231
231
  <span class="shape-property-label" style="flex-shrink: 0; width: 64px; text-align: left;">图表类型</span>
232
- <el-popover placement="bottom" :width="380" trigger="click" popper-class="chart-popover-popper"
232
+ <el-popover placement="bottom" :width="568" trigger="click" popper-class="chart-popover-popper"
233
233
  v-model:visible="chartTypePopoverVisible" :show-arrow="false">
234
234
  <template #reference>
235
235
  <div
@@ -477,8 +477,8 @@ function toggleFillSwitch(val: any) {
477
477
  }
478
478
 
479
479
  .shape-accordion-header-label {
480
- font-size: 13px;
481
- font-weight: 600;
480
+ font-size: 14px;
481
+ font-weight: 400;
482
482
  color: var(--text-primary);
483
483
  display: flex;
484
484
  align-items: center;
@@ -7,7 +7,7 @@ import QuickColorPopover from '../../QuickColorPopover.vue'
7
7
  import GradientEditor from './GradientEditor.vue'
8
8
  import { useSlides, gradientPresets, normalizeSlideBackground } from '../../../composables/useSlides'
9
9
  import { SYS_FONTS } from '../../../configs/fonts'
10
- import { PATTERN_PRESETS, parseColor, generateGradientEndColor } from '../../../composables/sidebar/useSidebarShapeFormat'
10
+ import { PATTERN_PRESETS, parseColor, hexToRgba, generateGradientEndColor, buildPatternSvgDataUrl, parsePatternSvgMeta } from '../../../composables/sidebar/useSidebarShapeFormat'
11
11
 
12
12
  const props = defineProps<{
13
13
  designActiveTab: string
@@ -81,6 +81,8 @@ const quickRecentColors = ref<string[]>([])
81
81
  const globalFontColorPopoverVisible = ref(false)
82
82
  const globalBorderColorPopoverVisible = ref(false)
83
83
  const bgColorPopoverVisible = ref(false)
84
+ const bgPatternFgPopoverVisible = ref(false)
85
+ const bgPatternBgPopoverVisible = ref(false)
84
86
  const isCustomColorPickerOpen = ref(false)
85
87
 
86
88
  // 画布尺寸下拉 Popover 状态
@@ -399,8 +401,11 @@ const bgFillType = computed({
399
401
  if (isExistingSvgPattern && existingUrl) {
400
402
  activeSlide.value.background = `url("${existingUrl}") left top / auto repeat`
401
403
  } else {
402
- const defaultPattern = PATTERN_PRESETS[0]?.url || ''
403
- activeSlide.value.background = defaultPattern ? `url("${defaultPattern}") left top / auto repeat` : ''
404
+ const pType = (activeSlide.value as any).bgPatternType || 'dots-sparse'
405
+ const pFg = (activeSlide.value as any).bgPatternFg || '#3B82F6'
406
+ const pBg = (activeSlide.value as any).bgPatternBg || '#FFFFFF'
407
+ const patUrl = buildPatternSvgDataUrl(pType, pFg, pBg)
408
+ activeSlide.value.background = `url("${patUrl}") left top / auto repeat`
404
409
  }
405
410
  }
406
411
  }
@@ -555,6 +560,80 @@ const bgImagePreviewStyle = computed(() => {
555
560
  }
556
561
  })
557
562
 
563
+ const activeBgPatternMeta = computed(() => {
564
+ const rawUrl = extractUrlFromBgCss(activeSlide.value?.background)
565
+ return parsePatternSvgMeta(rawUrl)
566
+ })
567
+
568
+ const activeBgPatternType = computed(() => {
569
+ return (activeSlide.value as any)?.bgPatternType || activeBgPatternMeta.value.type || 'dots-sparse'
570
+ })
571
+
572
+ const activeBgPatternFg = computed(() => {
573
+ return (activeSlide.value as any)?.bgPatternFg || activeBgPatternMeta.value.fg || '#3B82F6'
574
+ })
575
+
576
+ const activeBgPatternBg = computed(() => {
577
+ return (activeSlide.value as any)?.bgPatternBg || activeBgPatternMeta.value.bg || '#FFFFFF'
578
+ })
579
+
580
+ const parsedBgPatternFg = computed(() => parseColor(activeBgPatternFg.value))
581
+ const parsedBgPatternBg = computed(() => parseColor(activeBgPatternBg.value))
582
+
583
+ function getBgPatternPresetPreviewUrl(p: { key: string; url: string }) {
584
+ return buildPatternSvgDataUrl(p.key, activeBgPatternFg.value, activeBgPatternBg.value)
585
+ }
586
+
587
+ function isBgPatternPresetActive(p: { key: string; url: string }) {
588
+ return activeBgPatternType.value === p.key
589
+ }
590
+
591
+ function applyBgPattern(type: string, fg: string, bg: string) {
592
+ if (!activeSlide.value) return
593
+ ;(activeSlide.value as any).bgPatternType = type
594
+ ;(activeSlide.value as any).bgPatternFg = fg
595
+ ;(activeSlide.value as any).bgPatternBg = bg
596
+ ;(activeSlide.value as any).isPattern = true
597
+ const patUrl = buildPatternSvgDataUrl(type, fg, bg)
598
+ activeSlide.value.background = `url("${patUrl}") left top / auto repeat`
599
+ activeSlide.value.bgType = 'image'
600
+ addHistorySnapshot()
601
+ }
602
+
603
+ function selectBgPatternPreset(p: { key: string; url: string }) {
604
+ applyBgPattern(p.key, activeBgPatternFg.value, activeBgPatternBg.value)
605
+ }
606
+
607
+ function onSelectBgPatternFgColor(color: string) {
608
+ const fg = (!color || color === 'transparent' || color === 'none') ? 'transparent' : color
609
+ applyBgPattern(activeBgPatternType.value, fg, activeBgPatternBg.value)
610
+ }
611
+
612
+ function handleBgPatternFgOpacityInput(e: Event) {
613
+ const target = e.target as HTMLInputElement
614
+ let val = parseInt(target.value)
615
+ if (isNaN(val)) val = 100
616
+ val = Math.max(0, Math.min(100, val))
617
+ const hex = parsedBgPatternFg.value.hex === 'transparent' ? '#3B82F6' : parsedBgPatternFg.value.hex
618
+ const fg = val === 0 ? 'transparent' : (val === 100 ? hex : hexToRgba(hex, val))
619
+ applyBgPattern(activeBgPatternType.value, fg, activeBgPatternBg.value)
620
+ }
621
+
622
+ function onSelectBgPatternBgColor(color: string) {
623
+ const bg = (!color || color === 'transparent' || color === 'none') ? 'transparent' : color
624
+ applyBgPattern(activeBgPatternType.value, activeBgPatternFg.value, bg)
625
+ }
626
+
627
+ function handleBgPatternBgOpacityInput(e: Event) {
628
+ const target = e.target as HTMLInputElement
629
+ let val = parseInt(target.value)
630
+ if (isNaN(val)) val = 100
631
+ val = Math.max(0, Math.min(100, val))
632
+ const hex = parsedBgPatternBg.value.hex === 'transparent' ? '#FFFFFF' : parsedBgPatternBg.value.hex
633
+ const bg = val === 0 ? 'transparent' : (val === 100 ? hex : hexToRgba(hex, val))
634
+ applyBgPattern(activeBgPatternType.value, activeBgPatternFg.value, bg)
635
+ }
636
+
558
637
  const bgFileInputRef = ref<HTMLInputElement | null>(null)
559
638
  const uploadBgImage = () => {
560
639
  bgFileInputRef.value?.click()
@@ -1042,7 +1121,7 @@ watch(extractTab, () => {
1042
1121
  </svg>
1043
1122
  </button>
1044
1123
  <button type="button" class="fill-type-icon-btn" :class="{ active: activeBgTab === 'pattern' }"
1045
- @click="selectBgTab('pattern')" title="纹理填充">
1124
+ @click="selectBgTab('pattern')" title="图案填充">
1046
1125
  <svg viewBox="0 0 18 18" width="18" height="18">
1047
1126
  <defs>
1048
1127
  <pattern id="bgFillBarPattern" width="4" height="4" patternUnits="userSpaceOnUse"
@@ -1141,12 +1220,68 @@ watch(extractTab, () => {
1141
1220
  </div>
1142
1221
  </div>
1143
1222
 
1144
- <!-- 纹理填充卡片 -->
1223
+ <!-- 图案填充卡片 -->
1145
1224
  <template v-else-if="isPatternBg">
1146
- <div class="pattern-presets-grid" style="margin-bottom: 8px;">
1147
- <div v-for="p in PATTERN_PRESETS" :key="p.name" class="pattern-preset-card"
1148
- :class="{ active: bgImageUrl === p.url }" :title="p.name" :style="{ backgroundImage: `url(${p.url})` }"
1149
- @click="bgImageUrl = p.url"></div>
1225
+ <div class="pattern-fill-container">
1226
+ <div class="pattern-presets-grid">
1227
+ <div v-for="p in PATTERN_PRESETS" :key="p.key" class="pattern-preset-card"
1228
+ :class="{ active: isBgPatternPresetActive(p) }" :title="p.name"
1229
+ :style="{ backgroundImage: `url('${getBgPatternPresetPreviewUrl(p)}')` }"
1230
+ @click="selectBgPatternPreset(p)"></div>
1231
+ </div>
1232
+
1233
+ <!-- 前景 / 背景 颜色选择 (对齐 PPT / WPS 图案填充) -->
1234
+ <div class="pattern-colors-section">
1235
+ <div class="pattern-color-row">
1236
+ <span class="pattern-color-label">前景</span>
1237
+ <div class="full-color-pill" style="flex: 1;">
1238
+ <QuickColorPopover v-model:visible="bgPatternFgPopoverVisible"
1239
+ :color="parsedBgPatternFg.hex === 'transparent' ? '#3B82F6' : parsedBgPatternFg.hex"
1240
+ :alpha="parsedBgPatternFg.alpha" v-model:recentColors="quickRecentColors"
1241
+ @select-color="onSelectBgPatternFgColor">
1242
+ <template #reference>
1243
+ <div class="color-left-info">
1244
+ <div class="color-square-preview"
1245
+ :style="{ backgroundColor: parsedBgPatternFg.hex === 'transparent' ? 'transparent' : parsedBgPatternFg.hex }">
1246
+ </div>
1247
+ <span class="color-hex-text">{{ formatHexDisplay(parsedBgPatternFg.hex) }}</span>
1248
+ </div>
1249
+ </template>
1250
+ </QuickColorPopover>
1251
+ <div class="color-opacity-input-wrapper" @click.stop title="前景不透明度">
1252
+ <input type="number" class="color-opacity-num-input" :value="parsedBgPatternFg.alpha" min="0"
1253
+ max="100" @input="handleBgPatternFgOpacityInput"
1254
+ @keydown.enter="($event.target as HTMLElement).blur()" />
1255
+ <span class="percent-unit">%</span>
1256
+ </div>
1257
+ </div>
1258
+ </div>
1259
+
1260
+ <div class="pattern-color-row">
1261
+ <span class="pattern-color-label">背景</span>
1262
+ <div class="full-color-pill" style="flex: 1;">
1263
+ <QuickColorPopover v-model:visible="bgPatternBgPopoverVisible"
1264
+ :color="parsedBgPatternBg.hex === 'transparent' ? '#FFFFFF' : parsedBgPatternBg.hex"
1265
+ :alpha="parsedBgPatternBg.alpha" v-model:recentColors="quickRecentColors"
1266
+ @select-color="onSelectBgPatternBgColor">
1267
+ <template #reference>
1268
+ <div class="color-left-info">
1269
+ <div class="color-square-preview"
1270
+ :style="{ backgroundColor: parsedBgPatternBg.hex === 'transparent' ? 'transparent' : parsedBgPatternBg.hex }">
1271
+ </div>
1272
+ <span class="color-hex-text">{{ formatHexDisplay(parsedBgPatternBg.hex) }}</span>
1273
+ </div>
1274
+ </template>
1275
+ </QuickColorPopover>
1276
+ <div class="color-opacity-input-wrapper" @click.stop title="背景不透明度">
1277
+ <input type="number" class="color-opacity-num-input" :value="parsedBgPatternBg.alpha" min="0"
1278
+ max="100" @input="handleBgPatternBgOpacityInput"
1279
+ @keydown.enter="($event.target as HTMLElement).blur()" />
1280
+ <span class="percent-unit">%</span>
1281
+ </div>
1282
+ </div>
1283
+ </div>
1284
+ </div>
1150
1285
  </div>
1151
1286
  </template>
1152
1287
 
@@ -2072,6 +2207,32 @@ watch(extractTab, () => {
2072
2207
  user-select: none;
2073
2208
  }
2074
2209
 
2210
+ .pattern-fill-container {
2211
+ display: flex;
2212
+ flex-direction: column;
2213
+ gap: 10px;
2214
+ }
2215
+
2216
+ .pattern-colors-section {
2217
+ display: flex;
2218
+ flex-direction: column;
2219
+ gap: 8px;
2220
+ padding-top: 2px;
2221
+ }
2222
+
2223
+ .pattern-color-row {
2224
+ display: flex;
2225
+ align-items: center;
2226
+ gap: 8px;
2227
+ }
2228
+
2229
+ .pattern-color-label {
2230
+ width: 36px;
2231
+ font-size: 13px;
2232
+ color: #475569;
2233
+ flex-shrink: 0;
2234
+ }
2235
+
2075
2236
  .pattern-presets-grid {
2076
2237
  display: grid;
2077
2238
  grid-template-columns: repeat(4, 1fr);