Browse Source

Merge branch 'beta' of https://git.cocorobo.cn/jack/PPT into beta

lsc 4 days ago
parent
commit
2e2f243b05

+ 0 - 5
src/App.vue

@@ -141,11 +141,6 @@ window.addEventListener('beforeunload', () => {
   height: 100%;
 }
 
-sub {
-  display: grid;
-  align-items: center;
-}
-
 .image-preview {
   position: fixed;
   inset: 0;

+ 1 - 2
src/assets/styles/prosemirror.scss

@@ -59,8 +59,7 @@
     font-size: smaller;
   }
   sub {
-    vertical-align: sub;
-    font-size: smaller;
+    vertical-align: baseline;
   }
 
   blockquote {

+ 0 - 1
src/components/CollapsibleToolbar/index.vue

@@ -383,7 +383,6 @@ const getTypeClass = (type?: number) => {
   font-weight: 500;
   color: #6b7280;
   text-align: center;
-  line-height: 1.2;
 }
 
 .sidebar-item:hover .item-label,

+ 104 - 11
src/hooks/useImport.ts

@@ -29,12 +29,27 @@ import type {
 
 const convertFontSizePtToPx = (html: string, ratio: number) => {
   //return html;
-  return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
-    return `font-size: ${(parseFloat(p1) * ratio) | 0}px`
+  return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
+    return `${(parseFloat(p1) * ratio - 1) | 0}px `
   })
 
 }
 
+const getStyle = (htmlString: string) => {
+  //return html;
+  // 1. 创建 DOMParser 实例
+  const parser = new DOMParser();
+  // 2. 解析 HTML 字符串为文档对象
+  const doc = parser.parseFromString(htmlString, 'text/html');
+  // 3. 获取 p 元素
+  const p = doc.querySelector('p');
+  // 4. 读取 style 属性(内联样式字符串)
+  const styleAttr = p?.getAttribute('allstyle');
+  console.log(styleAttr); // 输出完整的 style 字符串
+  return styleAttr || "";
+
+}
+
 export default () => {
   const slidesStore = useSlidesStore()
   const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(useSlidesStore())
@@ -1077,6 +1092,8 @@ export default () => {
       // 计算缩放比例
       let ratio = 96 / 72; // PPTX 默认 72 DPI,屏幕 96 DPI
       const width = json.size.width;
+      const height = json.size.height;
+      const viewportRatio = json.size.viewportRatio || (height && width ? height / width : undefined)
       if (fixedViewport) {
         ratio = 1000 / width; // 固定视口宽度为 1000px
       } else {
@@ -1133,9 +1150,10 @@ export default () => {
         };
 
         // ----- 解析元素(递归函数)-----
-        const parseElements = async (elements: any[]) => {
+        const parseElements = async (elements: any[], pelements: any = null) => {
           // 按绘制顺序排序
           const sortedElements = elements.sort((a, b) => a.order - b.order);
+          console.log(sortedElements)
 
           for (const el of sortedElements) {
             // 保存原始尺寸用于后续可能的路径计算
@@ -1143,6 +1161,11 @@ export default () => {
             const originHeight = el.height || 1;
             const originLeft = el.left;
             const originTop = el.top;
+            // 保存原始尺寸用于后续可能的路径计算
+            const poriginWidth = pelements?.width;
+            const poriginHeight = pelements?.height;
+            const poriginLeft = pelements?.left;
+            const poriginTop = pelements?.top;
 
             // 应用缩放
             el.width = el.width * ratio;
@@ -1162,7 +1185,8 @@ export default () => {
                 defaultFontName: theme.value.fontName,
                 defaultColor: theme.value.fontColor,
                 content: convertFontSizePtToPx(el.content, ratio),
-                lineHeight: 1,
+                style: getStyle(convertFontSizePtToPx(el.content, ratio)),
+                lineHeight: 1.5,
                 outline: {
                   color: el.borderColor,
                   width: +(el.borderWidth * ratio).toFixed(2),
@@ -1267,6 +1291,7 @@ export default () => {
                 })();
                 uploadTasks.push(uploadTask);
               }
+              
 
               slide.elements.push(element)
 
@@ -1339,6 +1364,7 @@ export default () => {
 
               slide.elements.push(element);
             }
+            
 
             // ---------- 形状 ----------
             else if (el.type === 'shape') {
@@ -1390,6 +1416,7 @@ export default () => {
                   },
                   text: {
                     content: convertFontSizePtToPx(el.content, ratio),
+                    style: getStyle(convertFontSizePtToPx(el.content, ratio)),
                     defaultFontName: theme.value.fontName,
                     defaultColor: theme.value.fontColor,
                     align: vAlignMap[el.vAlign] || 'middle',
@@ -1409,12 +1436,13 @@ export default () => {
 
                 if (shape) {
                   element.path = shape.path;
+                  //const { maxX, maxY } = getSvgPathRange(el.path);
                   element.viewBox = shape.viewBox;
-
+                  //element.viewBox = [originWidth || maxX, originHeight || maxY];
                   if (shape.pathFormula) {
                     element.pathFormula = shape.pathFormula;
                     element.viewBox = [el.width, el.height];
-
+                    //element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
                     const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula];
                     if ('editable' in pathFormula && pathFormula.editable) {
                       element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue);
@@ -1423,10 +1451,14 @@ export default () => {
                       element.path = pathFormula.formula(el.width, el.height);
                     }
                   }
-                } else if (el.path && el.path.indexOf('NaN') === -1) {
+                }
+                else if (el.path && el.path.indexOf('NaN') === -1) {
                   const { maxX, maxY } = getSvgPathRange(el.path);
                   element.path = el.path;
-                  element.viewBox = [maxX || originWidth, maxY || originHeight];
+                  element.viewBox = poriginWidth? [maxX, maxY]: [originWidth, originHeight];
+                  //element.viewBox = [originWidth || maxX, originHeight || maxY];  
+                  //element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
+                  //element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
                 }
 
                 if (el.shapType === 'custom') {
@@ -1439,7 +1471,12 @@ export default () => {
                     element.path = el.path!;
                   }
                   const { maxX, maxY } = getSvgPathRange(element.path);
-                  element.viewBox = [maxX || originWidth, maxY || originHeight];
+                  element.viewBox = [maxX, maxY];
+                  //element.viewBox = [originWidth || maxX, originHeight || maxY];  
+                  //element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
+                  //element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];  
+                  //element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
+                  //element.viewBox = [originWidth, originHeight];
                 }
 
                 if (element.path) slide.elements.push(element);
@@ -1634,7 +1671,7 @@ export default () => {
               if (el.isFlipV) elements = flipGroupElements(elements, 'x');
 
               // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
-              await parseElements(elements);
+              await parseElements(elements, el);
             }
 
             // ---------- 图表组合(SmartArt)----------
@@ -1644,7 +1681,7 @@ export default () => {
                 left: _el.left + originLeft,
                 top: _el.top + originTop,
               }));
-              await parseElements(elements);
+              await parseElements(elements, el);
             }
           }
         };
@@ -1672,6 +1709,62 @@ export default () => {
       Promise.all(uploadTasks);
 
       exporting.value = false;
+
+      /*
+      // 更新视口尺寸(如果提供了的话)
+      if (width !== undefined && height !== undefined) {
+        console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
+        
+        // 同时也要更新slidesStore中的相关数据
+        if (slidesStore.setViewportSize) {
+          console.log('正在更新store中的视口尺寸')
+          slidesStore.setViewportSize(width)
+          if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
+            slidesStore.setViewportRatio(viewportRatio)
+            console.log('视口比例已更新:', viewportRatio)
+          }
+        }
+        
+        window.dispatchEvent(new CustomEvent('viewportSizeUpdated', { 
+          detail: { width, height, viewportRatio }
+        }))
+        console.log('视口尺寸更新事件已触发')
+      }
+      
+      // 导入成功后,触发画布尺寸更新
+      // 使用 nextTick 确保DOM更新完成后再触发
+      console.log('开始触发画布尺寸更新事件...')
+      nextTick(() => {
+        console.log('DOM更新完成,触发 slidesDataUpdated 事件')
+        // 触发自定义事件,通知需要更新画布尺寸的组件
+        window.dispatchEvent(new CustomEvent('slidesDataUpdated', { 
+          detail: { 
+            slides, 
+            cover,
+            title,
+            theme,
+            width,
+            height,
+            viewportRatio,
+            timestamp: Date.now()
+          } 
+        }))
+        console.log('slidesDataUpdated 事件已触发')
+        
+        // 检查并调整幻灯片索引,确保在有效范围内
+        const newSlideCount = slides.length
+        const currentIndex = slidesStore.slideIndex
+        if (currentIndex >= newSlideCount) {
+          console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
+          slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
+        }
+        
+        console.log('画布尺寸更新事件处理完成')
+        
+
+      })
+*/
+
     };
 
     reader.readAsArrayBuffer(file);

+ 2 - 0
src/types/slides.ts

@@ -185,6 +185,7 @@ export interface PPTTextElement extends PPTBaseElement {
   paragraphSpace?: number
   vertical?: boolean
   textType?: TextType
+  style?: string
 }
 
 
@@ -311,6 +312,7 @@ export interface ShapeText {
   defaultColor: string
   align: ShapeTextAlign
   type?: TextType
+  style?: string
 }
 
 /**

+ 1 - 1
src/utils/prosemirror/schema/marks.ts

@@ -10,7 +10,7 @@ const subscript: MarkSpec = {
       getAttrs: value => value === 'sub' && null
     },
   ],
-  toDOM: () => ['sub', 0],
+  toDOM: () => ['sub',  0],
 }
 
 const superscript: MarkSpec = {

+ 1 - 2
src/views/components/element/ShapeElement/BaseShapeElement.vue

@@ -138,8 +138,7 @@ const text = computed<ShapeText>(() => {
   right: 0;
   display: flex;
   flex-direction: column;
-  padding: 10px;
-  line-height: 1.2;
+  padding: 5px;
   word-break: break-word;
 
   &.top {

+ 21 - 15
src/views/components/element/ShapeElement/index.vue

@@ -67,19 +67,20 @@
           </g>
         </svg>
 
-        <div class="shape-text" :class="[text.align, { 'editable': editable || text.content }]">
-          <ProsemirrorEditor
-            ref="prosemirrorEditorRef"
-            v-if="editable || text.content"
-            :elementId="elementInfo.id"
-            :defaultColor="text.defaultColor"
-            :defaultFontName="text.defaultFontName"
-            :editable="!elementInfo.lock"
-            :value="text.content"
-            @update="({ value, ignore }) => updateText(value, ignore)"
-            @blur="checkEmptyText()"
-            @mousedown="$event => handleSelectElement($event, false)"
-          />
+        <div class="shape-text" :style="text.style" :class="[text.align, { 'editable': editable || text.content }]">
+            <ProsemirrorEditor
+              ref="prosemirrorEditorRef"
+              v-if="editable || text.content"
+              :elementId="elementInfo.id"
+              :defaultColor="text.defaultColor"
+              :defaultFontName="text.defaultFontName"
+              :editable="!elementInfo.lock"
+              :value="text.content"
+              @update="({ value, ignore }) => updateText(value, ignore)"
+              @blur="checkEmptyText()"
+              @mousedown="$event => handleSelectElement($event, false)"
+            />
+
         </div>
       </div>
     </div>
@@ -229,6 +230,8 @@ const startEdit = () => {
   }
 }
 .shape-text {
+  width:100%;
+  height:100%;
   position: absolute;
   top: 0;
   bottom: 0;
@@ -236,8 +239,7 @@ const startEdit = () => {
   right: 0;
   display: flex;
   flex-direction: column;
-  padding: 10px;
-  line-height: 1.2;
+  padding: 5px;
   word-break: break-word;
   pointer-events: none;
 
@@ -250,6 +252,10 @@ const startEdit = () => {
   }
   &.middle {
     justify-content: center;
+    left: 50%;
+    top: 50%;
+    -webkit-transform: translate(-50%,-50%);
+    transform: translate(-50%,-50%);
   }
   &.bottom {
     justify-content: flex-end;

+ 3 - 0
src/views/components/element/TextElement/BaseTextElement.vue

@@ -25,6 +25,9 @@
           color: elementInfo.defaultColor,
           fontFamily: elementInfo.defaultFontName,
           writingMode: elementInfo.vertical ? 'vertical-rl' : 'horizontal-tb',
+          display: 'flex',
+          alignItems: 'center',
+          overflow: hidden,
         }"
       >
         <ElementOutline

+ 48 - 16
src/views/components/element/TextElement/index.vue

@@ -27,9 +27,7 @@
           color: elementInfo.defaultColor,
           fontFamily: elementInfo.defaultFontName,
           writingMode: elementInfo.vertical ? 'vertical-rl' : 'horizontal-tb',
-          display: 'flex',
-          alignItems: 'center',
-          overflow: hidden,
+
         }"
         v-contextmenu="contextmenus"
         @mousedown="$event => handleSelectElement($event)"
@@ -40,19 +38,21 @@
           :height="elementInfo.height"
           :outline="elementInfo.outline"
         />
-        <ProsemirrorEditor
-          class="text"
-          :elementId="elementInfo.id"
-          :defaultColor="elementInfo.defaultColor"
-          :defaultFontName="elementInfo.defaultFontName"
-          :editable="!elementInfo.lock"
-          :value="elementInfo.content"
-          :style="{
-            '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
-          }"
-          @update="({ value, ignore }) => updateContent(value, ignore)"
-          @mousedown="$event => handleSelectElement($event, false)"
-        />
+        <div class="shape-text" :style="elementInfo.style" :class="[elementInfo.align, { 'editable': editable || elementInfo.content }]">
+          <ProsemirrorEditor
+            class="text"
+            :elementId="elementInfo.id"
+            :defaultColor="elementInfo.defaultColor"
+            :defaultFontName="elementInfo.defaultFontName"
+            :editable="!elementInfo.lock"
+            :value="elementInfo.content"
+            :style="{
+              '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
+            }"
+            @update="({ value, ignore }) => updateContent(value, ignore)"
+            @mousedown="$event => handleSelectElement($event, false)"
+          />
+        </div>
 
         <!-- 当字号过大且行高较小时,会出现文字高度溢出的情况,导致拖拽区域无法被选中,因此添加了以下节点避免该情况 -->
         <div class="drag-handler top"></div>
@@ -220,4 +220,36 @@ watch(isHandleElement, () => {
     bottom: 0;
   }
 }
+
+.shape-text {
+  width:100%;
+  height:100%;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  flex-direction: column;
+  word-break: break-word;
+  pointer-events: none;
+
+  &.editable {
+    pointer-events: all;
+  }
+
+  &.top {
+    justify-content: flex-start;
+  }
+  &.middle {
+    justify-content: center;
+    left: 50%;
+    top: 50%;
+    -webkit-transform: translate(-50%,-50%);
+    transform: translate(-50%,-50%);
+  }
+  &.bottom {
+    justify-content: flex-end;
+  }
+}
 </style>