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
@@ -10,10 +10,12 @@ import GradientEditor from './GradientEditor.vue'
10
10
  import { SHAPE_LIST } from '../../../utils/shapes'
11
11
  import { isImagePlaceholder, getElementGradientConfig, getElementGradientCss } from '../../../utils/canvas/elementStyles'
12
12
  import { isEllipseShape } from '../../../utils/line'
13
- import { PATTERN_PRESETS, parseColor, generateGradientEndColor } from '../../../composables/sidebar/useSidebarShapeFormat'
13
+ import { PATTERN_PRESETS, parseColor, hexToRgba, generateGradientEndColor, buildPatternSvgDataUrl, parsePatternSvgMeta } from '../../../composables/sidebar/useSidebarShapeFormat'
14
14
 
15
15
  const shapePopoverVisible = ref(false)
16
16
  const fillPopoverVisible = ref(false)
17
+ const patternFgPopoverVisible = ref(false)
18
+ const patternBgPopoverVisible = ref(false)
17
19
  const imageFileInputRef = ref<HTMLInputElement | null>(null)
18
20
 
19
21
  const props = defineProps<{
@@ -353,8 +355,84 @@ function handleImageFileChange(e: Event) {
353
355
  }
354
356
  }
355
357
 
356
- function selectPatternPreset(url: string) {
357
- emit('update:shapePatternFill', url)
358
+ const activePatternType = computed(() => {
359
+ if (props.shapePatternType && props.shapePatternType !== 'pattern' && props.shapePatternType !== 'solid') {
360
+ return props.shapePatternType
361
+ }
362
+ const meta = parsePatternSvgMeta(currentPatternImage.value)
363
+ return meta.type || 'diag-up'
364
+ })
365
+
366
+ const parsedPatternFg = computed(() => {
367
+ const raw = props.shapePatternFg || parsePatternSvgMeta(currentPatternImage.value).fg || '#3B82F6'
368
+ return parseColor(raw)
369
+ })
370
+
371
+ const parsedPatternBg = computed(() => {
372
+ const raw = props.shapePatternBg || parsePatternSvgMeta(currentPatternImage.value).bg || '#FFFFFF'
373
+ return parseColor(raw)
374
+ })
375
+
376
+ function getPatternPresetPreviewUrl(p: { key: string; url: string }) {
377
+ const fg = props.shapePatternFg || '#3B82F6'
378
+ const bg = props.shapePatternBg || '#FFFFFF'
379
+ return buildPatternSvgDataUrl(p.key, fg, bg)
380
+ }
381
+
382
+ function isPatternPresetActive(p: { key: string; url: string }) {
383
+ return activePatternType.value === p.key
384
+ }
385
+
386
+ function selectPatternPreset(p: { key: string; url: string }) {
387
+ emit('update:shapePatternType', p.key)
388
+ }
389
+
390
+ function onSelectPatternFgColor(color: string) {
391
+ if (!color || color === 'transparent' || color === 'none') {
392
+ emit('update:shapePatternFg', 'transparent')
393
+ return
394
+ }
395
+ emit('update:shapePatternFg', color)
396
+ }
397
+
398
+ function handlePatternFgOpacityInput(e: Event) {
399
+ const target = e.target as HTMLInputElement
400
+ let val = parseInt(target.value)
401
+ if (isNaN(val)) val = 100
402
+ val = Math.max(0, Math.min(100, val))
403
+ if (val === 0) {
404
+ emit('update:shapePatternFg', 'transparent')
405
+ } else if (val === 100) {
406
+ const hex = parsedPatternFg.value.hex === 'transparent' ? '#3B82F6' : parsedPatternFg.value.hex
407
+ emit('update:shapePatternFg', hex)
408
+ } else {
409
+ const hex = parsedPatternFg.value.hex === 'transparent' ? '#3B82F6' : parsedPatternFg.value.hex
410
+ emit('update:shapePatternFg', hexToRgba(hex, val))
411
+ }
412
+ }
413
+
414
+ function onSelectPatternBgColor(color: string) {
415
+ if (!color || color === 'transparent' || color === 'none') {
416
+ emit('update:shapePatternBg', 'transparent')
417
+ return
418
+ }
419
+ emit('update:shapePatternBg', color)
420
+ }
421
+
422
+ function handlePatternBgOpacityInput(e: Event) {
423
+ const target = e.target as HTMLInputElement
424
+ let val = parseInt(target.value)
425
+ if (isNaN(val)) val = 100
426
+ val = Math.max(0, Math.min(100, val))
427
+ if (val === 0) {
428
+ emit('update:shapePatternBg', 'transparent')
429
+ } else if (val === 100) {
430
+ const hex = parsedPatternBg.value.hex === 'transparent' ? '#FFFFFF' : parsedPatternBg.value.hex
431
+ emit('update:shapePatternBg', hex)
432
+ } else {
433
+ const hex = parsedPatternBg.value.hex === 'transparent' ? '#FFFFFF' : parsedPatternBg.value.hex
434
+ emit('update:shapePatternBg', hexToRgba(hex, val))
435
+ }
358
436
  }
359
437
 
360
438
  const displayFillOpacity = computed(() => {
@@ -578,7 +656,7 @@ function onSelectFillColor(color: string) {
578
656
  </svg>
579
657
  </button>
580
658
  <button type="button" class="fill-type-icon-btn" :class="{ active: activeFillMode === 'pattern' }"
581
- @click="selectFillMode('pattern')" title="纹理填充">
659
+ @click="selectFillMode('pattern')" title="图案填充">
582
660
  <svg viewBox="0 0 18 18" width="18" height="18">
583
661
  <defs>
584
662
  <pattern id="fillBarPattern" width="4" height="4" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
@@ -663,16 +741,85 @@ function onSelectFillColor(color: string) {
663
741
 
664
742
  <!-- Pattern Fill -->
665
743
  <template v-else-if="shapeFillType === 'pattern'">
666
- <div class="pattern-presets-grid">
667
- <div
668
- v-for="p in PATTERN_PRESETS"
669
- :key="p.name"
670
- class="pattern-preset-card"
671
- :class="{ active: currentPatternImage === p.url }"
672
- :title="p.name"
673
- :style="{ backgroundImage: `url(${p.url})` }"
674
- @click="selectPatternPreset(p.url)"
675
- ></div>
744
+ <div class="pattern-fill-container">
745
+ <div class="pattern-presets-grid">
746
+ <div
747
+ v-for="p in PATTERN_PRESETS"
748
+ :key="p.key"
749
+ class="pattern-preset-card"
750
+ :class="{ active: isPatternPresetActive(p) }"
751
+ :title="p.name"
752
+ :style="{ backgroundImage: `url('${getPatternPresetPreviewUrl(p)}')` }"
753
+ @click="selectPatternPreset(p)"
754
+ ></div>
755
+ </div>
756
+
757
+ <!-- 前景 / 背景 颜色选择 (对齐 PPT / WPS 图案填充) -->
758
+ <div class="pattern-colors-section">
759
+ <div class="shape-property-row">
760
+ <span class="shape-property-label" style="width: 44px;">前景</span>
761
+ <div class="full-color-pill" style="flex: 1;">
762
+ <QuickColorPopover
763
+ v-model:visible="patternFgPopoverVisible"
764
+ :color="parsedPatternFg.hex === 'transparent' ? '#3B82F6' : parsedPatternFg.hex"
765
+ :alpha="parsedPatternFg.alpha"
766
+ :recent-colors="recentColors"
767
+ @select-color="onSelectPatternFgColor"
768
+ >
769
+ <template #reference>
770
+ <div class="color-left-info">
771
+ <div :style="{ background: parsedPatternFg.hex === 'transparent' ? 'transparent' : parsedPatternFg.hex }" class="color-square-preview"></div>
772
+ <span class="color-hex-text">{{ formatHexDisplay(parsedPatternFg.hex) }}</span>
773
+ </div>
774
+ </template>
775
+ </QuickColorPopover>
776
+ <div class="color-opacity-input-wrapper" @click.stop title="前景不透明度">
777
+ <input
778
+ type="number"
779
+ class="color-opacity-num-input"
780
+ :value="parsedPatternFg.alpha"
781
+ min="0"
782
+ max="100"
783
+ @input="handlePatternFgOpacityInput"
784
+ @keydown.enter="($event.target as HTMLElement).blur()"
785
+ />
786
+ <span class="percent-unit">%</span>
787
+ </div>
788
+ </div>
789
+ </div>
790
+
791
+ <div class="shape-property-row">
792
+ <span class="shape-property-label" style="width: 44px;">背景</span>
793
+ <div class="full-color-pill" style="flex: 1;">
794
+ <QuickColorPopover
795
+ v-model:visible="patternBgPopoverVisible"
796
+ :color="parsedPatternBg.hex === 'transparent' ? '#FFFFFF' : parsedPatternBg.hex"
797
+ :alpha="parsedPatternBg.alpha"
798
+ :recent-colors="recentColors"
799
+ @select-color="onSelectPatternBgColor"
800
+ >
801
+ <template #reference>
802
+ <div class="color-left-info">
803
+ <div :style="{ background: parsedPatternBg.hex === 'transparent' ? 'transparent' : parsedPatternBg.hex }" class="color-square-preview"></div>
804
+ <span class="color-hex-text">{{ formatHexDisplay(parsedPatternBg.hex) }}</span>
805
+ </div>
806
+ </template>
807
+ </QuickColorPopover>
808
+ <div class="color-opacity-input-wrapper" @click.stop title="背景不透明度">
809
+ <input
810
+ type="number"
811
+ class="color-opacity-num-input"
812
+ :value="parsedPatternBg.alpha"
813
+ min="0"
814
+ max="100"
815
+ @input="handlePatternBgOpacityInput"
816
+ @keydown.enter="($event.target as HTMLElement).blur()"
817
+ />
818
+ <span class="percent-unit">%</span>
819
+ </div>
820
+ </div>
821
+ </div>
822
+ </div>
676
823
  </div>
677
824
  </template>
678
825
  </div>
@@ -798,8 +945,8 @@ function onSelectFillColor(color: string) {
798
945
  }
799
946
 
800
947
  .shape-accordion-header-label {
801
- font-size: 13px;
802
- font-weight: 500;
948
+ font-size: 14px;
949
+ font-weight: 400;
803
950
  color: #1e293b;
804
951
  display: flex;
805
952
  align-items: center;
@@ -1149,6 +1296,19 @@ function onSelectFillColor(color: string) {
1149
1296
  }
1150
1297
  }
1151
1298
 
1299
+ .pattern-fill-container {
1300
+ display: flex;
1301
+ flex-direction: column;
1302
+ gap: 10px;
1303
+ }
1304
+
1305
+ .pattern-colors-section {
1306
+ display: flex;
1307
+ flex-direction: column;
1308
+ gap: 8px;
1309
+ padding-top: 2px;
1310
+ }
1311
+
1152
1312
  .pattern-presets-grid {
1153
1313
  display: grid;
1154
1314
  grid-template-columns: repeat(4, 1fr);
@@ -426,8 +426,8 @@ function applyTheme(theme: any) {
426
426
  }
427
427
 
428
428
  .shape-accordion-header-label {
429
- font-size: 13px;
430
- font-weight: 600;
429
+ font-size: 14px;
430
+ font-weight: 400;
431
431
  color: var(--text-primary);
432
432
  display: flex;
433
433
  align-items: center;
@@ -1420,8 +1420,8 @@ function startDragAngle(e: MouseEvent) {
1420
1420
  }
1421
1421
 
1422
1422
  .accordion-title {
1423
- font-size: 13px;
1424
- font-weight: 500;
1423
+ font-size: 14px;
1424
+ font-weight: 400;
1425
1425
  color: #1e293b;
1426
1426
  }
1427
1427
 
@@ -123,20 +123,18 @@ function playPreview(transitionType?: string) {
123
123
 
124
124
  const scale = (zoomRatio?.value || 100) / 100
125
125
 
126
- if (t === 'none') {
127
- if (activeAnimation) {
128
- activeAnimation.cancel()
129
- activeAnimation = null
130
- }
131
- restoreSlideTransform()
132
- return
133
- }
134
-
135
126
  if (activeAnimation) {
127
+ activeAnimation.onfinish = null
128
+ activeAnimation.oncancel = null
136
129
  activeAnimation.cancel()
137
130
  activeAnimation = null
138
131
  }
139
132
 
133
+ if (t === 'none') {
134
+ restoreSlideTransform()
135
+ return
136
+ }
137
+
140
138
  let actualType = t
141
139
  if (t === 'random') {
142
140
  const pool = [
@@ -220,7 +218,6 @@ function playPreview(transitionType?: string) {
220
218
 
221
219
  // 临时防止滚动条跳动
222
220
  const wrapper = document.querySelector('.canvas-slide-wrapper') as HTMLElement
223
- const originalOverflow = wrapper ? wrapper.style.overflow : ''
224
221
  if (wrapper) {
225
222
  wrapper.style.overflow = 'hidden'
226
223
  }
@@ -228,16 +225,15 @@ function playPreview(transitionType?: string) {
228
225
  slideEl.style.transformOrigin = 'center center'
229
226
  slideEl.style.pointerEvents = 'none'
230
227
 
231
- activeAnimation = slideEl.animate(keyframes, {
228
+ const currentAnim = slideEl.animate(keyframes, {
232
229
  duration: dur,
233
230
  easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)'
234
231
  })
232
+ activeAnimation = currentAnim
235
233
 
236
234
  const cleanup = () => {
237
- if (activeAnimation) {
238
- activeAnimation.cancel()
239
- activeAnimation = null
240
- }
235
+ if (activeAnimation !== currentAnim) return
236
+ activeAnimation = null
241
237
  if (slideEl) {
242
238
  // 关键:强制保留当前缩放比例 scale(scale),绝不能置空清除!
243
239
  slideEl.style.transform = `scale(${scale})`
@@ -247,17 +243,30 @@ function playPreview(transitionType?: string) {
247
243
  slideEl.style.pointerEvents = ''
248
244
  }
249
245
  if (wrapper) {
250
- wrapper.style.overflow = originalOverflow
246
+ wrapper.style.overflow = ''
251
247
  }
252
248
  }
253
249
 
254
- activeAnimation.onfinish = cleanup
255
- activeAnimation.oncancel = cleanup
250
+ currentAnim.onfinish = cleanup
251
+ currentAnim.oncancel = cleanup
252
+ }
253
+
254
+ const hoveredTransition = ref<string | null>(null)
255
+
256
+ function onHoverTransition(val: string) {
257
+ if (hoveredTransition.value === val) return
258
+ hoveredTransition.value = val
259
+ playPreview(val)
260
+ }
261
+
262
+ function onLeaveTransition(val: string) {
263
+ if (hoveredTransition.value === val) {
264
+ hoveredTransition.value = null
265
+ }
256
266
  }
257
267
 
258
268
  function selectTransition(val: string) {
259
269
  emit('update:activeSlideTransition', val)
260
- playPreview(val)
261
270
  }
262
271
 
263
272
  const transitionIcons: Record<string, { default: string; selected: string }> = {
@@ -313,6 +322,8 @@ function handleApplyAll() {
313
322
  :key="item.value"
314
323
  class="transition-item"
315
324
  :class="{ active: activeSlideTransition === item.value }"
325
+ @mouseenter="onHoverTransition(item.value)"
326
+ @mouseleave="onLeaveTransition(item.value)"
316
327
  @click="selectTransition(item.value)"
317
328
  >
318
329
  <div class="transition-card">
@@ -350,6 +350,19 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
350
350
  if (typeof item.height === 'number') item.height = newH
351
351
  }
352
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
+
353
366
  // Auto-update font scaling for autoFit text boxes
354
367
  updateTextFontScale(item)
355
368
 
@@ -645,6 +658,9 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
645
658
  const geom = getElementPixels(curEl)
646
659
  const actualW = geom.width || 200
647
660
  const actualH = geom.height || 200
661
+ if (curEl.pathFormula === 'roundRect') {
662
+ curEl.viewBox = [actualW, actualH]
663
+ }
648
664
  const viewBoxW = curEl.viewBox?.[0] || 200
649
665
  const viewBoxH = curEl.viewBox?.[1] || 200
650
666
 
@@ -1048,6 +1064,14 @@ export function useCanvasTransform(options: CanvasTransformOptions) {
1048
1064
  const newPath = formulaObj.formula(viewBoxW, viewBoxH, curEl.keypoints)
1049
1065
  curEl.content = newPath
1050
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
+ }
1051
1075
 
1052
1076
  const parentGroup = findParentGroup(curEl.id)
1053
1077
  if (parentGroup && parentGroup.elements) {