jvs-ppt-ui 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/dist/lib/jvs-ppt-ui.es.js +22404 -21454
- package/dist/lib/jvs-ppt-ui.umd.js +102 -101
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +53 -53
- package/src/components/AIPptDialog.vue +2 -0
- package/src/components/CanvasArea.vue +164 -72
- package/src/components/Header.vue +374 -144
- package/src/components/PptEditor.vue +33 -15
- package/src/components/PresentationMode.vue +24 -4
- package/src/components/SidebarRight.vue +8 -1
- package/src/components/SvgChart.vue +5 -2
- package/src/components/TemplateGallery.vue +1321 -866
- package/src/components/Toolbar.vue +244 -22
- package/src/components/canvas/CanvasTransformHandles.vue +5 -1
- package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
- package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
- package/src/components/canvas/textSelectionOverlay.ts +305 -0
- package/src/composables/canvas/useCanvasAlignment.ts +286 -238
- package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
- package/src/composables/canvas/useCanvasFormatting.ts +35 -12
- package/src/composables/canvas/useCanvasTable.ts +52 -24
- package/src/composables/canvas/useCanvasText.ts +12 -0
- package/src/composables/canvas/useCanvasTransform.ts +15 -2
- package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
- package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
- package/src/composables/useSlides.ts +495 -132
- package/src/style.css +88 -0
- package/src/types/index.ts +8 -0
- package/src/utils/line.ts +274 -312
- package/src/utils/pptxShapes.ts +1 -1
- package/src/utils/pptxShapesLazy.ts +39 -0
- package/src/utils/svgPathParser.ts +12 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, useSlots, getCurrentInstance, defineAsyncComponent, type Ref } from 'vue'
|
|
2
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, useSlots, getCurrentInstance, defineAsyncComponent, toRaw, type Ref } from 'vue'
|
|
3
3
|
import jvsPickerColorV3 from 'jvs-picker-color-v3'
|
|
4
4
|
import '../assets/iconfont/iconfont.js'
|
|
5
5
|
import { saveAs } from 'file-saver'
|
|
@@ -33,6 +33,8 @@ const props = withDefaults(defineProps<PptEditorProps>(), {
|
|
|
33
33
|
modelValue: null,
|
|
34
34
|
readonly: false,
|
|
35
35
|
showHeader: true,
|
|
36
|
+
showMenu: true,
|
|
37
|
+
toolbarMode: 'auto',
|
|
36
38
|
showSidebarLeft: true,
|
|
37
39
|
showSidebarRight: true,
|
|
38
40
|
enableMcp: false,
|
|
@@ -63,6 +65,7 @@ const emit = defineEmits<{
|
|
|
63
65
|
const slidesState = useSlides()
|
|
64
66
|
const {
|
|
65
67
|
slides,
|
|
68
|
+
slidesRevision,
|
|
66
69
|
activeIndex,
|
|
67
70
|
activeSlide,
|
|
68
71
|
presentationTitle,
|
|
@@ -211,8 +214,10 @@ if (props.config?.title) {
|
|
|
211
214
|
function getDeck(): DeckData {
|
|
212
215
|
return {
|
|
213
216
|
title: presentationTitle.value || '未命名演示文稿',
|
|
214
|
-
|
|
215
|
-
|
|
217
|
+
// toRaw 序列化:避免 stringify 遍历响应式代理时把调用方的 active effect 订阅到整册
|
|
218
|
+
// (曾在 computed 里调用 getDeck 导致拖拽每步都整册深拷贝,页数越多越卡)
|
|
219
|
+
slides: JSON.parse(JSON.stringify(toRaw(slides.value) || [])),
|
|
220
|
+
slideMasters: JSON.parse(JSON.stringify(toRaw(slideMasters.value) || [])),
|
|
216
221
|
themeClrScheme: themeClrScheme.value,
|
|
217
222
|
slideRatio: slideRatio.value,
|
|
218
223
|
slideHeight: slideHeight.value,
|
|
@@ -769,16 +774,14 @@ function scheduleEmitDeck() {
|
|
|
769
774
|
}, 300)
|
|
770
775
|
}
|
|
771
776
|
|
|
772
|
-
// 监听内部 slides
|
|
773
|
-
watch
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
{ deep: true }
|
|
781
|
-
)
|
|
777
|
+
// 监听内部 slides 变化,向外部同步(拖拽/变形交互期间冻结,避免高频触发几十毫秒的深拷贝序列化)。
|
|
778
|
+
// 用 slidesRevision 版本号替代 slides deep watch:deep watch 每次变更都要整册深遍历重收集依赖,
|
|
779
|
+
// 100+ 页文档拖拽时每步移动都遍历全册(实测每步数百 ms);版本号由 useSlides 在各变更点 bump,O(1)
|
|
780
|
+
watch(slidesRevision, () => {
|
|
781
|
+
if (isInternalUpdating) return
|
|
782
|
+
if (isCanvasDragging?.value || pauseHistory?.value) return
|
|
783
|
+
scheduleEmitDeck()
|
|
784
|
+
})
|
|
782
785
|
|
|
783
786
|
// 拖拽/变形结束后,立即冲刷防抖中的对外同步
|
|
784
787
|
watch([() => isCanvasDragging?.value, () => pauseHistory?.value], ([isDragging, isPaused], [prevDragging, prevPaused]) => {
|
|
@@ -869,7 +872,10 @@ const slots = useSlots()
|
|
|
869
872
|
// 顶部工具栏插槽作用域上下文数据
|
|
870
873
|
const headerSlotContext = computed(() => ({
|
|
871
874
|
api: editorApi,
|
|
872
|
-
deck
|
|
875
|
+
// deck 必须走 getter:直接 getDeck() 会让 JSON.stringify 遍历整棵响应式树,本 computed 因此
|
|
876
|
+
// 订阅全册所有属性——拖拽/打字每步都重算并整册深拷贝(110 页实测每步 ~500ms)。getter 只在
|
|
877
|
+
// 宿主真正读取 deck 时才现算;且去掉 stringify 订阅后,本 computed 不再随页内元素变更失效
|
|
878
|
+
get deck() { return getDeck() },
|
|
873
879
|
slides: slidesState.slides.value,
|
|
874
880
|
activeSlide: slidesState.activeSlide.value || null,
|
|
875
881
|
activeIndex: slidesState.activeIndex.value,
|
|
@@ -895,6 +901,18 @@ const headerSlotContext = computed(() => ({
|
|
|
895
901
|
// 对外通过 ref 暴露所有方法
|
|
896
902
|
defineExpose(editorApi)
|
|
897
903
|
|
|
904
|
+
const computedShowMenu = computed(() => {
|
|
905
|
+
if (props.showMenu !== undefined) return props.showMenu
|
|
906
|
+
if (props.config?.showMenu !== undefined) return props.config.showMenu
|
|
907
|
+
return true
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
const computedToolbarMode = computed(() => {
|
|
911
|
+
if (props.toolbarMode !== undefined) return props.toolbarMode
|
|
912
|
+
if (props.config?.toolbarMode !== undefined) return props.config.toolbarMode
|
|
913
|
+
return 'auto'
|
|
914
|
+
})
|
|
915
|
+
|
|
898
916
|
onMounted(async () => {
|
|
899
917
|
// 触发 ready 事件
|
|
900
918
|
emit('ready', editorApi)
|
|
@@ -928,7 +946,7 @@ onMounted(async () => {
|
|
|
928
946
|
<div class="app-container jvs-ppt-editor-root" :class="{ 'is-readonly': readonly }">
|
|
929
947
|
<!-- Main top Navigation bar (支持完整 header 插槽替换或精细区域插槽定制) -->
|
|
930
948
|
<slot name="header" v-bind="headerSlotContext">
|
|
931
|
-
<Header ref="headerRef"
|
|
949
|
+
<Header ref="headerRef" :visible="showHeader" :show-menu="computedShowMenu" :toolbar-mode="computedToolbarMode" :slot-props="headerSlotContext">
|
|
932
950
|
<!-- 左侧前置 (Logo、返回按钮等) -->
|
|
933
951
|
<template v-if="slots['header-left-prefix']" #left-prefix="sp">
|
|
934
952
|
<slot name="header-left-prefix" v-bind="sp"></slot>
|
|
@@ -634,6 +634,26 @@ function getElementBorderWidth(el: any): number {
|
|
|
634
634
|
return 0
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
+
// 顶层文本"随文字调整大小"(autoFit=shape / width|height=auto):
|
|
638
|
+
// 画布(CanvasArea 顶层容器)按此规则用 auto 尺寸让文本框随内容撑开,横排撑高、竖排撑宽。
|
|
639
|
+
// 放映侧若钉死 style.height 渲染,画布里已撑开的文本会被压回 PPTX 原始小盒子 → 看起来拉伸/被裁
|
|
640
|
+
function isAutoSizeText(el: any): boolean {
|
|
641
|
+
if (el.type !== 'text') return false
|
|
642
|
+
if (el.isMaster || el.placeholder || el.isPlaceholder) return false
|
|
643
|
+
if ((el.content || '').includes('<table') || !!el.latex) return false
|
|
644
|
+
return true
|
|
645
|
+
}
|
|
646
|
+
function isAutoSizeHeightText(el: any): boolean {
|
|
647
|
+
if (!isAutoSizeText(el)) return false
|
|
648
|
+
if (el.style?.writingMode === 'vertical-rl' || el.style?.writingMode === 'vertical-lr') return false
|
|
649
|
+
return el.autoFit?.type === 'shape' || el.style?.height === 'auto'
|
|
650
|
+
}
|
|
651
|
+
function isAutoSizeWidthText(el: any): boolean {
|
|
652
|
+
if (!isAutoSizeText(el)) return false
|
|
653
|
+
if (el.style?.writingMode !== 'vertical-rl' && el.style?.writingMode !== 'vertical-lr') return false
|
|
654
|
+
return el.autoFit?.type === 'shape' || el.style?.width === 'auto'
|
|
655
|
+
}
|
|
656
|
+
|
|
637
657
|
// 组内子元素按 order / style.zIndex 升序 = 视觉层级顺序(与 CanvasArea sortGroupElements 一致)
|
|
638
658
|
function sortGroupChildren(elements: any[]): any[] {
|
|
639
659
|
if (!Array.isArray(elements) || elements.length <= 1) return elements || []
|
|
@@ -1177,8 +1197,8 @@ function getSafeShapeText(el: any): string {
|
|
|
1177
1197
|
position: 'absolute',
|
|
1178
1198
|
top: el.style.top,
|
|
1179
1199
|
left: el.style.left,
|
|
1180
|
-
width: el.style?.width || 'auto',
|
|
1181
|
-
height: el.style?.height || 'auto',
|
|
1200
|
+
width: isAutoSizeWidthText(el) ? 'auto' : (el.style?.width || 'auto'),
|
|
1201
|
+
height: isAutoSizeHeightText(el) ? 'auto' : (el.style?.height || 'auto'),
|
|
1182
1202
|
zIndex: 3,
|
|
1183
1203
|
transform: `rotate(${el.style.rotate || 0}deg) scale(${el.type !== 'text' && el.type !== 'line' && ((el.style as any)?.flipH || el.flipH || el.isFlipH) ? -1 : 1}, ${el.type !== 'text' && el.type !== 'line' && ((el.style as any)?.flipV || el.flipV || el.isFlipV) ? -1 : 1})`,
|
|
1184
1204
|
opacity: (el.style as any).opacity !== undefined ? (el.style as any).opacity : 1,
|
|
@@ -1253,7 +1273,7 @@ function getSafeShapeText(el: any): string {
|
|
|
1253
1273
|
flexDirection: el.style.verticalAlign ? 'column' : undefined,
|
|
1254
1274
|
justifyContent: (el.style.verticalAlign === 'middle' || el.style.verticalAlign === 'mid') ? 'center' : ((el.style.verticalAlign === 'bottom' || el.style.verticalAlign === 'down') ? 'flex-end' : undefined),
|
|
1255
1275
|
writingMode: (el.style.writingMode as any) || 'horizontal-tb',
|
|
1256
|
-
height: el.style.height || (el.style.verticalAlign || el.content.includes('<table') || el.latex ? '100%' : 'auto'),
|
|
1276
|
+
height: isAutoSizeHeightText(el) ? 'auto' : (el.style.height || (el.style.verticalAlign || el.content.includes('<table') || el.latex ? '100%' : 'auto')),
|
|
1257
1277
|
zoom: (el.autoFit?.type === 'text' && el.autoFit?.fontScale) ? (el.autoFit.fontScale / 100) : 1,
|
|
1258
1278
|
transform: 'none',
|
|
1259
1279
|
transformOrigin: 'top left',
|
|
@@ -1728,7 +1748,7 @@ function getSafeShapeText(el: any): string {
|
|
|
1728
1748
|
flexDirection: el.style.verticalAlign ? 'column' : undefined,
|
|
1729
1749
|
justifyContent: (el.style.verticalAlign === 'middle' || el.style.verticalAlign === 'mid') ? 'center' : ((el.style.verticalAlign === 'bottom' || el.style.verticalAlign === 'down') ? 'flex-end' : undefined),
|
|
1730
1750
|
writingMode: (el.style as any).writingMode || 'horizontal-tb',
|
|
1731
|
-
height: el.style.height || (el.style.verticalAlign || el.content.includes('<table') || el.latex ? '100%' : 'auto'),
|
|
1751
|
+
height: isAutoSizeHeightText(el) ? 'auto' : (el.style.height || (el.style.verticalAlign || el.content.includes('<table') || el.latex ? '100%' : 'auto')),
|
|
1732
1752
|
zoom: (el.autoFit?.type === 'text' && el.autoFit?.fontScale) ? (el.autoFit.fontScale / 100) : 1,
|
|
1733
1753
|
transform: 'none',
|
|
1734
1754
|
'--paragraphSpace': `${(el.style as any).paragraphSpace === undefined ? 3 : (el.style as any).paragraphSpace}px`
|
|
@@ -322,6 +322,7 @@ function getStyleVal(prop: string, fallback: any = ''): any {
|
|
|
322
322
|
else if (prop === 'fill' && el.fill) val = el.fill
|
|
323
323
|
else if (prop === 'borderColor' && el.borderColor) val = el.borderColor
|
|
324
324
|
else if (prop === 'fontFamily' && el.fontFamily) val = el.fontFamily
|
|
325
|
+
else if (prop === 'lineHeight') val = el.lineHeight || el.text?.lineHeight
|
|
325
326
|
}
|
|
326
327
|
if (prop === 'textAlign' && !val) {
|
|
327
328
|
const htmlContent = el.content || (typeof el.text === 'string' ? el.text : el.text?.content) || ''
|
|
@@ -1174,7 +1175,7 @@ function handleUndockToPopover() {
|
|
|
1174
1175
|
|
|
1175
1176
|
<template>
|
|
1176
1177
|
<!-- Right Sidebar: Inspector Panel -->
|
|
1177
|
-
<aside class="sidebar sidebar-right">
|
|
1178
|
+
<aside class="sidebar sidebar-right" :class="{ 'sidebar-wide-material': activeTab === 'material' || activeTab === 'library' }">
|
|
1178
1179
|
<!-- Sidebar Header -->
|
|
1179
1180
|
<div class="panel-tabs" style="align-items: center; justify-content: space-between; padding: 0 16px;">
|
|
1180
1181
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);">{{ sidebarTitle }}</span>
|
|
@@ -2043,6 +2044,12 @@ function handleUndockToPopover() {
|
|
|
2043
2044
|
display: flex;
|
|
2044
2045
|
flex-direction: column;
|
|
2045
2046
|
height: 100%;
|
|
2047
|
+
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
2048
|
+
|
|
2049
|
+
&.sidebar-wide-material {
|
|
2050
|
+
width: 380px !important;
|
|
2051
|
+
max-width: 90vw;
|
|
2052
|
+
}
|
|
2046
2053
|
}
|
|
2047
2054
|
|
|
2048
2055
|
.material-undock-btn {
|
|
@@ -388,12 +388,15 @@ const linePaths = computed<LinePath[]>(() => {
|
|
|
388
388
|
const max = maxVal.value
|
|
389
389
|
|
|
390
390
|
return datasets.value.map((ds, j) => {
|
|
391
|
-
const points = ds.data.map((val, i) => {
|
|
391
|
+
const points = (ds.data || []).map((val, i) => {
|
|
392
392
|
const x = padding.value.left + i * dx
|
|
393
393
|
const y = padding.value.top + chartHeight.value - (val / max) * chartHeight.value
|
|
394
394
|
return { x, y, val }
|
|
395
395
|
})
|
|
396
396
|
|
|
397
|
+
// 空数据系列会拼出 dLine="M "、dArea="M L ...",触发浏览器 <path> attribute d 控制台报错;整组跳过
|
|
398
|
+
if (points.length === 0) return null
|
|
399
|
+
|
|
397
400
|
const linePointsStr = points.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' L ')
|
|
398
401
|
const dLine = `M ${linePointsStr}`
|
|
399
402
|
|
|
@@ -409,7 +412,7 @@ const linePaths = computed<LinePath[]>(() => {
|
|
|
409
412
|
color: chartColors.value[j % chartColors.value.length],
|
|
410
413
|
points
|
|
411
414
|
}
|
|
412
|
-
})
|
|
415
|
+
}).filter((lp): lp is LinePath => lp !== null)
|
|
413
416
|
})
|
|
414
417
|
|
|
415
418
|
// Radar Grid levels and axis vectors
|