Browse Source

公式组件优化

dsy 1 month ago
parent
commit
fd55bb871b

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2026.07.02'
+VUE_APP_VERSION = '2026.07.03'

+ 144 - 5
src/components/PinyinText.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
   <div class="pinyin-area" :style="{ 'text-align': pinyinOverallPosition, padding: pinyinPadding }">
     <AudioPlay
@@ -104,6 +105,15 @@
             您的浏览器不支持音频播放
           </audio>
         </span>
+
+        <!-- 数学公式块(保留 MathJax 原始结构) -->
+        <MathHtml
+          v-else-if="block.type === 'math'"
+          :key="'math-' + index"
+          :html="block.html"
+          :style-obj="block.containerStyle"
+          :is-display="block.isDisplay"
+        />
         <!-- 换行符 -->
         <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
       </template>
@@ -176,11 +186,44 @@ import { sanitizeHTML } from '@/utils/common';
 import { isEnable } from '@/views/book/courseware/data/common';
 import AudioPlay from '@/views/book/courseware/preview/components/character_base/components/AudioPlay.vue';
 
+// MathHtml 组件用于渲染数学公式的 HTML 内容,保留 MathJax 的原始结构
+const MathHtml = {
+  name: 'MathHtml',
+  functional: true,
+  props: {
+    html: {
+      type: String,
+      default: '',
+    },
+    styleObj: {
+      type: Object,
+      default: () => ({}),
+    },
+    isDisplay: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  render(h, context) {
+    return h('span', {
+      class: {
+        'math-container': true,
+        'math-display': context.props.isDisplay,
+      },
+      style: context.props.styleObj,
+      domProps: {
+        innerHTML: sanitizeHTML(context.props.html || ''),
+      },
+    });
+  },
+};
+
 export default {
   name: 'PinyinText',
   components: {
     CorrectPinyin,
     AudioPlay,
+    MathHtml,
   },
   inject: ['convertText'],
   props: {
@@ -301,10 +344,10 @@ export default {
 
             // 如果找到了结束标签,合并完成,跳出内部循环
             if (nextItem.text && nextItem.text.includes('</video>')) {
-              j++; // 跳过结束标签
+              j += 1; // 跳过结束标签
               break;
             }
-            j++;
+            j += 1;
           }
 
           // 将合并后的内容作为一个新项加入结果
@@ -319,7 +362,7 @@ export default {
         } else {
           // 其他项直接加入
           result.push(item);
-          i++;
+          i += 1;
         }
       }
 
@@ -335,10 +378,33 @@ export default {
       let oldIndex = -1;
       let paragraphIndex = 0;
       const tagStack = [];
+      let mathContext = null;
 
       for (const item of listToParse) {
         oldIndex += 1;
 
+        if (mathContext) {
+          mathContext.html += this.getRawItemText(item);
+
+          if (item.is_style === 'true' || item.is_style === true) {
+            this.updateHtmlTagStack(mathContext.stack, item.text || '');
+            if (mathContext.stack.length === 0) {
+              blocks.push(this.parseMathBlock(mathContext.html, tagStack));
+              mathContext = null;
+            }
+          }
+          continue;
+        }
+
+        if (this.isMathRootTag(item)) {
+          const rootTag = this.extractTagName(item.text || '');
+          mathContext = {
+            html: this.getRawItemText(item),
+            stack: rootTag ? [rootTag] : ['span'],
+          };
+          continue;
+        }
+
         if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
           blocks.push(this.parseImageBlock(item, tagStack));
         } else if (item.text && typeof item.text === 'string' && item.text.includes('<video')) {
@@ -363,6 +429,10 @@ export default {
         }
       }
 
+      if (mathContext) {
+        blocks.push(this.parseMathBlock(mathContext.html, tagStack));
+      }
+
       return blocks;
     },
   },
@@ -628,6 +698,59 @@ export default {
       return styleObj;
     },
 
+    parseMathBlock(html, tagStack) {
+      const containerStyleObj = {};
+      tagStack.forEach((tagItem) => {
+        if (tagItem.style) {
+          this.mergeStyleString(containerStyleObj, tagItem.style);
+        }
+      });
+
+      const isDisplay =
+        /<mjx-container[^>]*\bdisplay=["']true["']/i.test(html) ||
+        /<mjx-assistive-mml[^>]*\bdisplay=["']block["']/i.test(html);
+
+      return {
+        type: 'math',
+        html,
+        containerStyle: containerStyleObj,
+        isDisplay,
+      };
+    },
+
+    isMathRootTag(item) {
+      if (!item || !(item.is_style === 'true' || item.is_style === true) || !item.text) return false;
+      return /class=["'][^"']*\bmathjax-container\b[^"']*\beditor-math\b[^"']*["']/i.test(item.text);
+    },
+
+    getRawItemText(item) {
+      if (!item) return '';
+      return item.text || '';
+    },
+
+    extractTagName(tagText) {
+      const match = (tagText || '').match(/^<\/?\s*([a-zA-Z][\w-]*)/);
+      return match ? match[1].toLowerCase() : '';
+    },
+
+    updateHtmlTagStack(tagStack, tagText) {
+      if (!tagText || !tagStack) return;
+
+      const tagName = this.extractTagName(tagText);
+      if (!tagName) return;
+
+      if (/^<\//.test(tagText)) {
+        for (let i = tagStack.length - 1; i >= 0; i--) {
+          if (tagStack[i] === tagName) {
+            tagStack.splice(i, 1);
+            break;
+          }
+        }
+      } else if (!/\/>\s*$/.test(tagText)) {
+        tagStack.push(tagName);
+      }
+    },
+
     getBlockColor(block) {
       const colorFromObject = block?.styleObj?.color;
       if (colorFromObject) return colorFromObject;
@@ -645,7 +768,7 @@ export default {
 
       const blockColor = this.getBlockColor(block);
       if (blockColor) {
-        baseStyle.color = `${blockColor} !important`;
+        baseStyle.color = blockColor;
       }
 
       if (this.isAllSetting) {
@@ -717,7 +840,7 @@ export default {
 
       const blockColor = this.getBlockColor(block);
       if (blockColor) {
-        styles['color'] = `${blockColor} !important`;
+        styles['color'] = blockColor;
       }
 
       // 如果设置了全局字号,优先使用全局字号
@@ -793,6 +916,22 @@ export default {
       margin: 0.5em 0;
     }
 
+    :deep(.mathjax-container.editor-math > svg[aria-hidden='true']) {
+      display: none !important;
+    }
+
+    .math-container.math-display {
+      display: block !important;
+      width: 100%;
+      text-align: center;
+    }
+
+    .math-container.math-display :deep(mjx-container[display='true']) {
+      display: block !important;
+      margin: 0 auto;
+      text-align: center;
+    }
+
     span {
       display: inline-flex;
     }

+ 9 - 4
src/components/RichText.vue

@@ -160,10 +160,6 @@ export default {
             font-family: 'League';
             src: url('${process.env.BASE_URL}static/font/pinyin.ttf');
           }
-          mjx-container, mjx-container * {
-            font-size: 16px !important; /* 强制固定字体 */
-            line-height: 1.2 !important; /* 避免行高影响 */
-          }
           mjx-assistive-mml {
             position: absolute !important;
             width: 1px !important;
@@ -188,6 +184,15 @@ export default {
             padding-bottom: 0.15em; /* 间距也相对于字体 */
             display: inline;
           }
+          /* 多行公式居中显示 */
+          .editor-math mjx-container[display='true'] {
+            display: block !important;
+            margin: 0.8em auto !important;
+            text-align: center !important;
+          }
+          .editor-math mjx-container[display='true'] > svg {
+            margin: 0 auto !important;
+          }
           `, // 解决公式每点击一次字体就变大
         valid_elements: '*[*]', // 允许所有标签和属性
         valid_children: '+body[style]', // 允许 MathJax 的样式

+ 4 - 3
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -1,3 +1,4 @@
+<!-- eslint-disable vue/no-v-html -->
 <template>
   <ModuleBase ref="base" :type="data.type">
     <template #content>
@@ -209,7 +210,7 @@ export default {
       immediate: true,
     },
     'data.content': {
-      handler(newVal, oldVal) {
+      handler(newVal) {
         this.handlerMindMap();
         if (!this.inited && newVal) {
           this.parseFClist();
@@ -233,7 +234,7 @@ export default {
     },
   },
   methods: {
-    uploads(file_id, file_url) {
+    uploads(file_id) {
       this.data.audio_file_id = file_id;
     },
     deleteFiles() {
@@ -546,7 +547,7 @@ export default {
       this.isViewExplanatoryNoteDialog = false;
     },
     // 设置备注
-    selectContentSetMemo(data, noteId) {
+    selectContentSetMemo(data) {
       if (!data) return;
       // 统一 ID 字段,方便查找
       const currentId = data.annota_id || data.id;