Просмотр исходного кода

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

lsc 4 недель назад
Родитель
Сommit
c7c8e30da0

+ 19 - 4
src/hooks/useImport.ts

@@ -1,4 +1,4 @@
-import { ref, nextTick } from 'vue'
+import { ref, nextTick } from 'vue'
 import { storeToRefs } from 'pinia'
 import { parse, type Shape, type Element, type ChartItem, type BaseElement } from 'pptxtojson'
 import { nanoid } from 'nanoid'
@@ -1823,7 +1823,7 @@ export default () => {
                 defaultColor: theme.value.fontColor,
                 content: convertFontSizePtToPx(el.content, ratio, el.autoFit),
                 style: getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)),
-                lineHeight: 1.5,
+                lineHeight: 1.2,
                 align: vAlignMap[el.vAlign] || 'middle',
                 outline: {
                   color: el.borderColor,
@@ -2051,8 +2051,23 @@ export default () => {
                 : undefined
 
               const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
-              const fill = el.fill?.type === 'color' ? el.fill.value : 'none'
-              const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
+              const fill = el.fill?.type === 'color' ? el.fill.value : el.fill || 'none'
+              //const style = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit)) + (el.pathBBox?.pWidth ? ';width:' + (el.pathBBox?.pWidth * ratio) + 'px;height:' + (el.pathBBox?.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
+              const baseStyle = getStyle(convertFontSizePtToPx(el.content, ratio, el.autoFit));
+              const isVertical =
+                (typeof el.content === 'string' && el.content.includes('writing-mode: vertical-rl')) ||
+                baseStyle.includes('writing-mode: vertical-rl');
+
+              let extraStyle = '';
+              if (el.pathBBox?.pWidth) {
+                const w = el.pathBBox.pWidth * ratio;
+                const h = el.pathBBox.pHeight * ratio;
+                extraStyle = isVertical
+                  ? `;width:${h}px;height:${w}px;`
+                  : `;width:${w}px;height:${h}px;`;
+              }
+
+              const style = baseStyle + extraStyle;
               const element: PPTShapeElement = {
                 type: 'shape',
                 id: nanoid(10),

+ 137 - 18
src/utils/prosemirror/schema/marks.ts

@@ -1,6 +1,7 @@
 import { marks } from 'prosemirror-schema-basic'
 import type { MarkSpec } from 'prosemirror-model'
 
+// ===== 原有定义 =====
 const subscript: MarkSpec = {
   excludes: 'subscript',
   parseDOM: [
@@ -10,7 +11,7 @@ const subscript: MarkSpec = {
       getAttrs: value => value === 'sub' && null
     },
   ],
-  toDOM: () => ['sub',  0],
+  toDOM: () => ['sub', 0],
 }
 
 const superscript: MarkSpec = {
@@ -56,9 +57,7 @@ const underline: MarkSpec = {
 }
 
 const forecolor: MarkSpec = {
-  attrs: {
-    color: {},
-  },
+  attrs: { color: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -76,9 +75,7 @@ const forecolor: MarkSpec = {
 }
 
 const backcolor: MarkSpec = {
-  attrs: {
-    backcolor: {},
-  },
+  attrs: { backcolor: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -96,9 +93,7 @@ const backcolor: MarkSpec = {
 }
 
 const fontsize: MarkSpec = {
-  attrs: {
-    fontsize: {},
-  },
+  attrs: { fontsize: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
@@ -116,16 +111,14 @@ const fontsize: MarkSpec = {
 }
 
 const fontname: MarkSpec = {
-  attrs: {
-    fontname: {},
-  },
+  attrs: { fontname: {} },
   inline: true,
   group: 'inline',
   parseDOM: [
     {
       style: 'font-family',
       getAttrs: fontname => {
-        return { fontname: fontname && typeof fontname === 'string' ? fontname.replace(/[\"\']/g, '') : '' }
+        return { fontname: fontname && typeof fontname === 'string' ? fontname.replace(/["']/g, '') : '' }
       }
     },
   ],
@@ -158,9 +151,7 @@ const link: MarkSpec = {
 }
 
 const mark: MarkSpec = {
-  attrs: {
-    index: { default: null },
-  },
+  attrs: { index: { default: null } },
   parseDOM: [
     {
       tag: 'mark',
@@ -173,14 +164,135 @@ const mark: MarkSpec = {
   toDOM: node => ['mark', { 'data-index': node.attrs.index }, 0],
 }
 
+// ===== 新增 marks =====
+
+// 通用 font-weight(如 100, 200, bold, bolder 等)
+const fontWeight: MarkSpec = {
+  attrs: { fontWeight: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'font-weight',
+      getAttrs: value => value ? { fontWeight: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { fontWeight } = mark.attrs
+    let style = ''
+    if (fontWeight) style += `font-weight: ${fontWeight};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 font-style(如 italic, oblique, normal)
+const fontStyle: MarkSpec = {
+  attrs: { fontStyle: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'font-style',
+      getAttrs: value => value ? { fontStyle: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { fontStyle } = mark.attrs
+    let style = ''
+    if (fontStyle) style += `font-style: ${fontStyle};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 text-decoration(可包含 underline, overline, line-through 等组合)
+const textDecoration: MarkSpec = {
+  attrs: { textDecoration: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'text-decoration',
+      getAttrs: value => value ? { textDecoration: value } : {}
+    },
+    {
+      style: 'text-decoration-line',
+      getAttrs: value => value ? { textDecoration: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { textDecoration } = mark.attrs
+    let style = ''
+    if (textDecoration) style += `text-decoration: ${textDecoration};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 通用 vertical-align(支持 baseline, sub, super, top, middle, bottom, 长度值等)
+const verticalAlign: MarkSpec = {
+  attrs: { verticalAlign: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'vertical-align',
+      getAttrs: value => value ? { verticalAlign: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { verticalAlign } = mark.attrs
+    let style = ''
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`
+    return ['span', { style }, 0]
+  }
+}
+
+// letter-spacing
+const letterSpacing: MarkSpec = {
+  attrs: { letterSpacing: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'letter-spacing',
+      getAttrs: value => value ? { letterSpacing: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { letterSpacing } = mark.attrs
+    let style = ''
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`
+    return ['span', { style }, 0]
+  }
+}
+
+// text-shadow
+const textShadow: MarkSpec = {
+  attrs: { textShadow: {} },
+  inline: true,
+  group: 'inline',
+  parseDOM: [
+    {
+      style: 'text-shadow',
+      getAttrs: value => value ? { textShadow: value } : {}
+    }
+  ],
+  toDOM: mark => {
+    const { textShadow } = mark.attrs
+    let style = ''
+    if (textShadow) style += `text-shadow: ${textShadow};`
+    return ['span', { style }, 0]
+  }
+}
+
+// 从 prosemirror-schema-basic 获取的默认 marks
 const { em, strong, code } = marks
 
 export default {
   em,
   strong,
+  code,
   fontsize,
   fontname,
-  code,
   forecolor,
   backcolor,
   subscript,
@@ -189,4 +301,11 @@ export default {
   underline,
   link,
   mark,
+  // 新增
+  fontWeight,
+  fontStyle,
+  textDecoration,
+  verticalAlign,
+  letterSpacing,
+  textShadow,
 }

+ 111 - 52
src/utils/prosemirror/schema/nodes.ts

@@ -115,7 +115,16 @@ const listItem: NodeSpec = {
     paddingRight: { default: '' },
     paddingBottom: { default: '' },
     paddingLeft: { default: '' },
-    whiteSpace: { default: 'normal' }, // 新增 white-space 属性
+    whiteSpace: { default: 'normal' },
+    writingMode: { default: '' },
+    // 新增字体相关属性
+    fontWeight: { default: '' },
+    fontStyle: { default: '' },
+    textDecoration: { default: '' },
+    textDecorationLine: { default: '' },
+    verticalAlign: { default: '' },
+    letterSpacing: { default: '' },
+    textShadow: { default: '' },
   },
   content: 'paragraph block*',
   group: 'block',
@@ -138,7 +147,17 @@ const listItem: NodeSpec = {
         const paddingRight = style.paddingRight || '';
         const paddingBottom = style.paddingBottom || '';
         const paddingLeft = style.paddingLeft || '';
-        const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
+        const whiteSpace = style.whiteSpace || 'normal';
+        const writingMode = style.writingMode || '';
+
+        // 读取新增属性
+        const fontWeight = style.fontWeight || '';
+        const fontStyle = style.fontStyle || '';
+        const textDecoration = style.textDecoration || '';
+        const textDecorationLine = style.textDecorationLine || '';
+        const verticalAlign = style.verticalAlign || '';
+        const letterSpacing = style.letterSpacing || '';
+        const textShadow = style.textShadow || '';
 
         return {
           textAlign,
@@ -153,7 +172,15 @@ const listItem: NodeSpec = {
           paddingRight,
           paddingBottom,
           paddingLeft,
-          whiteSpace, // 返回 whiteSpace
+          whiteSpace,
+          writingMode,
+          fontWeight,
+          fontStyle,
+          textDecoration,
+          textDecorationLine,
+          verticalAlign,
+          letterSpacing,
+          textShadow,
         };
       },
     },
@@ -172,7 +199,15 @@ const listItem: NodeSpec = {
       paddingRight,
       paddingBottom,
       paddingLeft,
-      whiteSpace, // 获取 whiteSpace
+      whiteSpace,
+      writingMode,
+      fontWeight,
+      fontStyle,
+      textDecoration,
+      textDecorationLine,
+      verticalAlign,
+      letterSpacing,
+      textShadow,
     } = node.attrs;
 
     let style = '';
@@ -189,13 +224,12 @@ const listItem: NodeSpec = {
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
     if (marginLeft) {
-      // 解析数值和单位
       const str = String(marginLeft).trim();
       const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
       if (match) {
         let num = parseFloat(match[1]);
         const unit = match[2] || 'px';
-        const absNum = Math.abs(num); // 负数转正
+        const absNum = Math.abs(num);
         const val = absNum + unit;
         style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
       } else {
@@ -206,35 +240,43 @@ const listItem: NodeSpec = {
     if (lineHeight) {
       let finalValue;
       const str = String(lineHeight).trim();
-      // 匹配纯数字(整数或小数,可带负号)
       if (/^-?\d+(\.\d+)?$/.test(str)) {
         finalValue = parseFloat(str) * 1.2;
       } else {
-        // 带单位或其他非纯数字内容,直接使用原值
         finalValue = lineHeight;
       }
       style += `line-height: ${finalValue};`;
     }
+    else{
+      style += `line-height: 1.2;`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
     if (paddingLeft) style += `padding-left: ${paddingLeft};`;
     if (whiteSpace && whiteSpace !== 'normal') {
-      style += `white-space: ${whiteSpace};`; // 添加 white-space
+      style += `white-space: ${whiteSpace};`;
+    }
+    if (writingMode) {
+      style += `writing-mode: ${writingMode};`;
     }
 
-    //const attrs: { style?: string } = {};
-    //if (style) attrs.style = style;
+    // 新增字体相关样式输出
+    if (fontWeight) style += `font-weight: ${fontWeight};`;
+    if (fontStyle) style += `font-style: ${fontStyle};`;
+    if (textDecoration) style += `text-decoration: ${textDecoration};`;
+    if (textDecorationLine) style += `text-decoration-line: ${textDecorationLine};`;
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`;
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`;
+    if (textShadow) style += `text-shadow: ${textShadow};`;
 
     let isEmpty = false;
     const firstChild = node.content.firstChild;
     if (firstChild && firstChild.type.name === 'paragraph') {
-      // 段落无任何子节点(包括 text 和 inline 节点)
       if (firstChild.content.size === 0) {
         isEmpty = true;
       }
     }
-    // 如果整个 li 的内容长度为 0 也可以作为判断
     if (node.content.size === 0) isEmpty = true;
 
     const attrs: { style?: string; class?: string } = {};
@@ -246,7 +288,7 @@ const listItem: NodeSpec = {
 };
 
 const paragraph: NodeSpec = {
-  whitespace: "pre", // 此属性控制 ProseMirror 内部空格处理,与 CSS white-space 无关,保留不变
+  whitespace: "pre",
   attrs: {
     textAlign: { default: '' },
     textAlignLast: { default: '' },
@@ -261,7 +303,16 @@ const paragraph: NodeSpec = {
     paddingRight: { default: '' },
     paddingBottom: { default: '' },
     paddingLeft: { default: '' },
-    whiteSpace: { default: 'normal' }, // 新增 white-space 属性
+    whiteSpace: { default: 'normal' },
+    writingMode: { default: '' },
+    // 新增字体相关属性
+    fontWeight: { default: '' },
+    fontStyle: { default: '' },
+    textDecoration: { default: '' },
+    textDecorationLine: { default: '' },
+    verticalAlign: { default: '' },
+    letterSpacing: { default: '' },
+    textShadow: { default: '' },
   },
   content: 'inline*',
   group: 'block',
@@ -284,7 +335,17 @@ const paragraph: NodeSpec = {
         const paddingRight = style.paddingRight || '';
         const paddingBottom = style.paddingBottom || '';
         const paddingLeft = style.paddingLeft || '';
-        const whiteSpace = style.whiteSpace || 'normal'; // 读取 white-space
+        const whiteSpace = style.whiteSpace || 'normal';
+        const writingMode = style.writingMode || '';
+
+        // 读取新增属性
+        const fontWeight = style.fontWeight || '';
+        const fontStyle = style.fontStyle || '';
+        const textDecoration = style.textDecoration || '';
+        const textDecorationLine = style.textDecorationLine || '';
+        const verticalAlign = style.verticalAlign || '';
+        const letterSpacing = style.letterSpacing || '';
+        const textShadow = style.textShadow || '';
 
         return {
           textAlign,
@@ -299,7 +360,15 @@ const paragraph: NodeSpec = {
           paddingRight,
           paddingBottom,
           paddingLeft,
-          whiteSpace, // 返回 whiteSpace
+          whiteSpace,
+          writingMode,
+          fontWeight,
+          fontStyle,
+          textDecoration,
+          textDecorationLine,
+          verticalAlign,
+          letterSpacing,
+          textShadow,
         };
       },
     },
@@ -326,7 +395,15 @@ const paragraph: NodeSpec = {
       paddingRight,
       paddingBottom,
       paddingLeft,
-      whiteSpace, // 获取 whiteSpace
+      whiteSpace,
+      writingMode,
+      fontWeight,
+      fontStyle,
+      textDecoration,
+      textDecorationLine,
+      verticalAlign,
+      letterSpacing,
+      textShadow,
     } = node.attrs;
 
     let style = '';
@@ -342,60 +419,42 @@ const paragraph: NodeSpec = {
     }
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
-    /*
-    if (marginLeft) {
-      // 解析数值和单位
-      const str = String(marginLeft).trim();
-      const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
-      if (match) {
-        let num = parseFloat(match[1]);
-        const unit = match[2] || 'px';
-        const absNum = Math.abs(num); // 负数转正
-        const val = absNum + unit;
-        style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
-      } else {
-        style += `margin-left: ${marginLeft};`;
-      }
-    }
-    */
     if (marginLeft) style += `margin-left: ${marginLeft};`;
     if (marginRight) style += `margin-right: ${marginRight};`;
     if (lineHeight) {
       let finalValue;
       const str = String(lineHeight).trim();
-      // 匹配纯数字(整数或小数,可带负号)
       if (/^-?\d+(\.\d+)?$/.test(str)) {
         finalValue = parseFloat(str) * 1.2;
       } else {
-        // 带单位或其他非纯数字内容,直接使用原值
         finalValue = lineHeight;
       }
       style += `line-height: ${finalValue};`;
     }
+    else{
+      style += `line-height: 1.2;`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
     if (paddingLeft) style += `padding-left: ${paddingLeft};`;
     if (whiteSpace && whiteSpace !== 'normal') {
-      style += `white-space: ${whiteSpace};`; // 添加 white-space
+      style += `white-space: ${whiteSpace};`;
     }
+    if (writingMode) {
+      style += `writing-mode: ${writingMode};`;
+    }
+
+    // 新增字体相关样式输出
+    if (fontWeight) style += `font-weight: ${fontWeight};`;
+    if (fontStyle) style += `font-style: ${fontStyle};`;
+    if (textDecoration) style += `text-decoration: ${textDecoration};`;
+    if (textDecorationLine) style += `text-decoration-line: ${textDecorationLine};`;
+    if (verticalAlign) style += `vertical-align: ${verticalAlign};`;
+    if (letterSpacing) style += `letter-spacing: ${letterSpacing};`;
+    if (textShadow) style += `text-shadow: ${textShadow};`;
 
     const attrs: { style?: string; class?: string } = {};
-/*
-    let isEmpty = false;
-    const firstChild = node.content.firstChild;
-    if (firstChild) {
-      // 段落无任何子节点(包括 text 和 inline 节点)
-      if (firstChild.content.size === 0) {
-        isEmpty = true;
-      }
-    }
-    
-    // 如果整个 li 的内容长度为 0 也可以作为判断
-    if (node.content.size === 0) isEmpty = true;
-    if (style) attrs.style = style;
-    //if (isEmpty) attrs.class = 'empty';
-*/
     if (style) attrs.style = style;
     return ['p', attrs, 0];
   },

+ 1 - 1
src/views/components/element/ImageElement/ImageOutline/ImageEllipseOutline.vue

@@ -10,7 +10,7 @@
       vector-effect="non-scaling-stroke" 
       stroke-linecap="butt" 
       stroke-miterlimit="8"
-      fill="transparent"
+      fill="#FFFFFF"
       :cx="width / 2" 
       :cy="height / 2"
       :rx="width / 2" 

+ 1 - 1
src/views/components/element/ImageElement/ImageOutline/ImagePolygonOutline.vue

@@ -10,7 +10,7 @@
       vector-effect="non-scaling-stroke" 
       stroke-linecap="butt" 
       stroke-miterlimit="8"
-      fill="transparent"
+      fill="#FFFFFF"
       :d="createPath(width, height)"
       :stroke="outlineColor"
       :stroke-width="outlineWidth" 

+ 1 - 1
src/views/components/element/ImageElement/ImageOutline/ImageRectOutline.vue

@@ -10,7 +10,7 @@
       vector-effect="non-scaling-stroke" 
       stroke-linecap="butt" 
       stroke-miterlimit="8"
-      fill="transparent"
+      fill="#FFFFFF"
       :rx="radius" 
       :ry="radius"
       :width="width"