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.
- package/dist/lib/jvs-ppt-ui.es.js +20301 -19823
- package/dist/lib/jvs-ppt-ui.umd.js +99 -99
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CanvasArea.vue +14 -115
- package/src/components/ChartTypeSelector.vue +34 -30
- package/src/components/Header.vue +40 -5
- package/src/components/PresentationGroupChild.vue +44 -13
- package/src/components/PresentationMode.vue +84 -33
- package/src/components/SidebarLeft.vue +96 -18
- package/src/components/SidebarRight.vue +6 -3
- package/src/components/Toolbar.vue +1 -1
- package/src/components/canvas/elements/CanvasShapeElement.vue +91 -34
- package/src/components/sidebar/common/SidebarBorderAccordion.vue +2 -2
- package/src/components/sidebar/common/SidebarReflectionAccordion.vue +2 -2
- package/src/components/sidebar/common/SidebarShadowAccordion.vue +2 -2
- package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +170 -9
- package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +176 -16
- package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
- package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +2 -2
- package/src/components/sidebar/panels/SidebarTransitionPanel.vue +30 -19
- package/src/composables/canvas/useCanvasTransform.ts +24 -0
- package/src/composables/sidebar/useSidebarShapeFormat.ts +248 -17
- package/src/composables/useSlides.ts +12 -0
- package/src/style.css +4 -2
- 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
|
-
|
|
357
|
-
|
|
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-
|
|
667
|
-
<div
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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:
|
|
802
|
-
font-weight:
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
246
|
+
wrapper.style.overflow = ''
|
|
251
247
|
}
|
|
252
248
|
}
|
|
253
249
|
|
|
254
|
-
|
|
255
|
-
|
|
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) {
|