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.
Files changed (34) hide show
  1. package/README.md +60 -2
  2. package/dist/lib/jvs-ppt-ui.es.js +22404 -21454
  3. package/dist/lib/jvs-ppt-ui.umd.js +102 -101
  4. package/dist/lib/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/App.vue +53 -53
  7. package/src/components/AIPptDialog.vue +2 -0
  8. package/src/components/CanvasArea.vue +164 -72
  9. package/src/components/Header.vue +374 -144
  10. package/src/components/PptEditor.vue +33 -15
  11. package/src/components/PresentationMode.vue +24 -4
  12. package/src/components/SidebarRight.vue +8 -1
  13. package/src/components/SvgChart.vue +5 -2
  14. package/src/components/TemplateGallery.vue +1321 -866
  15. package/src/components/Toolbar.vue +244 -22
  16. package/src/components/canvas/CanvasTransformHandles.vue +5 -1
  17. package/src/components/canvas/elements/CanvasShapeElement.vue +98 -19
  18. package/src/components/canvas/elements/CanvasTextElement.vue +39 -9
  19. package/src/components/canvas/textSelectionOverlay.ts +305 -0
  20. package/src/composables/canvas/useCanvasAlignment.ts +286 -238
  21. package/src/composables/canvas/useCanvasContextMenu.ts +53 -22
  22. package/src/composables/canvas/useCanvasFormatting.ts +35 -12
  23. package/src/composables/canvas/useCanvasTable.ts +52 -24
  24. package/src/composables/canvas/useCanvasText.ts +12 -0
  25. package/src/composables/canvas/useCanvasTransform.ts +15 -2
  26. package/src/composables/sidebar/useSidebarElementTransform.ts +5 -5
  27. package/src/composables/sidebar/useSidebarShapeFormat.ts +2 -2
  28. package/src/composables/useSlides.ts +495 -132
  29. package/src/style.css +88 -0
  30. package/src/types/index.ts +8 -0
  31. package/src/utils/line.ts +274 -312
  32. package/src/utils/pptxShapes.ts +1 -1
  33. package/src/utils/pptxShapesLazy.ts +39 -0
  34. 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
- slides: JSON.parse(JSON.stringify(slides.value || [])),
215
- slideMasters: JSON.parse(JSON.stringify(slideMasters.value || [])),
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
- () => slides.value,
775
- () => {
776
- if (isInternalUpdating) return
777
- if (isCanvasDragging?.value || pauseHistory?.value) return
778
- scheduleEmitDeck()
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: getDeck(),
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" v-show="showHeader" :slot-props="headerSlotContext">
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