Browse Source

富文本组件,拼音优化

zq 1 week ago
parent
commit
7177a48c4f

+ 174 - 129
src/components/PinyinText.vue

@@ -33,7 +33,7 @@
                   }"
                   }"
                 >
                 >
                   <span class="pinyin" :style="getPinyinStyle(word)"> {{ getCharPinyin(word, cIndex) }}</span>
                   <span class="pinyin" :style="getPinyinStyle(word)"> {{ getCharPinyin(word, cIndex) }}</span>
-                  <span class="py-char" :style="textStyle(word)">{{ convertText(char) }}</span>
+                  <span class="py-char" :style="getCharStyle(word, block, cIndex)">{{ convertText(char) }}</span>
                 </span>
                 </span>
               </span>
               </span>
             </span>
             </span>
@@ -231,143 +231,24 @@ export default {
       let oldIndex = -1;
       let oldIndex = -1;
       let paragraphIndex = 0;
       let paragraphIndex = 0;
       const tagStack = [];
       const tagStack = [];
+
       for (const item of this.richTextList) {
       for (const item of this.richTextList) {
         oldIndex += 1;
         oldIndex += 1;
 
 
         if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
         if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
-          // 处理图片
-          const imgMatch = item.text.match(/<img\s+([^>]*)\/?\s*>/i);
-          if (imgMatch) {
-            const attrs = imgMatch[1];
-            const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
-            const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
-            const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
-            const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
-            const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
-
-            // 获取父容器样式(从 tagStack 中累积)
-            const containerStyleObj = {};
-            tagStack.forEach((tagItem) => {
-              if (tagItem.style) {
-                tagItem.style.split(';').forEach((rule) => {
-                  if (rule.trim()) {
-                    const [prop, value] = rule.split(':').map((s) => s.trim());
-                    if (prop && value) {
-                      const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
-                      containerStyleObj[camelProp] = value;
-                    }
-                  }
-                });
-              }
-            });
-
-            // 解析图片自身样式
-            let imageStyleObj = {};
-            if (styleMatch) {
-              const styleStr = styleMatch[1];
-              styleStr.split(';').forEach((rule) => {
-                if (rule.trim()) {
-                  const [prop, value] = rule.split(':').map((s) => s.trim());
-                  if (prop && value) {
-                    const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
-                    imageStyleObj[camelProp] = value;
-                  }
-                }
-              });
-            }
-
-            blocks.push({
-              type: 'image',
-              src: srcMatch ? srcMatch[1] : '',
-              alt: altMatch ? altMatch[1] : '',
-              width: widthMatch ? widthMatch[1] : null,
-              height: heightMatch ? heightMatch[1] : null,
-              containerStyle: containerStyleObj,
-              imageStyle: imageStyleObj,
-            });
-          }
+          blocks.push(this.parseImageBlock(item, tagStack));
         } else if (item.is_style === 'true' || item.is_style === true) {
         } else if (item.is_style === 'true' || item.is_style === true) {
-          const tagText = item.text || '';
-          // 正则匹配闭标签:匹配 </tagname> 格式
-          const closeTagMatch = tagText.match(/^<\/(\w+)>$/);
-          if (closeTagMatch) {
-            // 闭标签:从栈顶开始查找最近的同名标签并移除(LIFO 原则)
-            const tagName = closeTagMatch[1];
-            this.removeTagFromStack(tagStack, tagName);
-          } else {
-            // 开标签:解析标签名和样式并压栈
-            const openTagMatch = tagText.match(/^<(\w+)([^>]*)>$/);
-            if (openTagMatch) {
-              const tagName = openTagMatch[1];
-              const attrs = openTagMatch[2];
-              // 解析 style 属性
-              const styleMatch = attrs.match(/style=["']([^"']*)["']/);
-              let style = styleMatch ? styleMatch[1] : null;
-
-              // 将无 style 属性的标签转换为对应的 CSS 样式
-              if (!style) {
-                const tagStyleMap = {
-                  strong: 'font-weight: bold',
-                  b: 'font-weight: bold',
-                  em: 'font-style: italic',
-                  i: 'font-style: italic',
-                  u: 'text-decoration: underline',
-                  s: 'text-decoration: line-through',
-                  del: 'text-decoration: line-through',
-                  sub: 'vertical-align: sub',
-                  sup: 'vertical-align: super',
-                };
-                style = tagStyleMap[tagName] || null;
-              }
-
-              tagStack.push({
-                tag: tagName,
-                style,
-              });
-            }
-          }
+          this.handleStyleTag(item, tagStack);
         } else if (item.text === '\n') {
         } else if (item.text === '\n') {
-          // 处理换行符
-          blocks.push({
-            type: 'newline',
-          });
+          blocks.push({ type: 'newline' });
           paragraphIndex += 1;
           paragraphIndex += 1;
         } else if (item.word_list && item.word_list.length > 0) {
         } else if (item.word_list && item.word_list.length > 0) {
-          // 处理文字内容:将当前标签栈中的样式应用到文字块
-          // 合并当前所有打开标签的样式
-          let combinedStyle = '';
-          tagStack.forEach((tagItem) => {
-            if (tagItem.style) {
-              combinedStyle += `${tagItem.style};`;
-            }
-          });
-
-          // 将样式字符串转换为对象格式
-          let styleObj = null;
-          if (combinedStyle) {
-            styleObj = {};
-            combinedStyle.split(';').forEach((rule) => {
-              if (rule.trim()) {
-                const [prop, value] = rule.split(':').map((s) => s.trim());
-                if (prop && value) {
-                  // 将驼峰命名转为 kebab-case(如 backgroundColor -> background-color)
-                  const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
-                  styleObj[camelProp] = value;
-                }
-              }
-            });
-          }
-
-          blocks.push({
-            type: 'text',
-            word_list: item.word_list,
-            index: textBlockIndex++,
-            oldIndex,
-            paragraphIndex,
-            styleObj, // 应用累积的样式对象
-          });
+          const hasEmphasisDot = this.checkHasEmphasisDot(tagStack);
+          blocks.push(this.createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot));
+          textBlockIndex += 1;
         }
         }
       }
       }
+
       return blocks;
       return blocks;
     },
     },
   },
   },
@@ -380,8 +261,167 @@ export default {
       deep: true,
       deep: true,
     },
     },
   },
   },
-
   methods: {
   methods: {
+    // 检查标签栈中是否有着重点样式
+    checkHasEmphasisDot(tagStack) {
+      return tagStack.some((tagItem) => {
+        if (!tagItem.className) return false;
+        return tagItem.className.includes('rich-text-emphasis-dot');
+      });
+    },
+
+    // 解析图片块
+    parseImageBlock(item, tagStack) {
+      const imgMatch = item.text.match(/<img\s+([^>]*)\/?\s*>/i);
+      if (!imgMatch) return null;
+
+      const attrs = imgMatch[1];
+      const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
+      const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
+      const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
+      const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
+      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+
+      const containerStyleObj = {};
+      tagStack.forEach((tagItem) => {
+        if (tagItem.style) {
+          this.mergeStyleString(containerStyleObj, tagItem.style);
+        }
+      });
+
+      const imageStyleObj = {};
+      if (styleMatch) {
+        this.mergeStyleString(imageStyleObj, styleMatch[1]);
+      }
+
+      return {
+        type: 'image',
+        src: srcMatch ? srcMatch[1] : '',
+        alt: altMatch ? altMatch[1] : '',
+        width: widthMatch ? widthMatch[1] : null,
+        height: heightMatch ? heightMatch[1] : null,
+        containerStyle: containerStyleObj,
+        imageStyle: imageStyleObj,
+      };
+    },
+
+    // 合并样式字符串到对象
+    mergeStyleString(styleObj, styleStr) {
+      styleStr.split(';').forEach((rule) => {
+        if (rule.trim()) {
+          const [prop, value] = rule.split(':').map((s) => s.trim());
+          if (prop && value) {
+            const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
+            styleObj[camelProp] = value;
+          }
+        }
+      });
+    },
+
+    // 处理样式标签
+    handleStyleTag(item, tagStack) {
+      const tagText = item.text || '';
+      const closeTagMatch = tagText.match(/^<\/(\w+)>$/);
+
+      if (closeTagMatch) {
+        const tagName = closeTagMatch[1];
+        this.removeTagFromStack(tagStack, tagName);
+      } else {
+        const openTagMatch = tagText.match(/^<(\w+)([^>]*)>$/);
+        if (openTagMatch) {
+          const tagName = openTagMatch[1];
+          const attrs = openTagMatch[2];
+          const { style, className } = this.extractTagInfo(attrs, tagName);
+          tagStack.push({ tag: tagName, style, className });
+        }
+      }
+    },
+
+    // 提取标签信息(样式和类名)
+    extractTagInfo(attrs, tagName) {
+      const styleMatch = attrs.match(/style=["']([^"']*)["']/);
+      let style = styleMatch ? styleMatch[1] : null;
+
+      const classMatch = attrs.match(/class=["']([^"']*)["']/);
+      const className = classMatch ? classMatch[1] : '';
+
+      if (!style) {
+        style = this.getDefaultTagStyle(tagName);
+      }
+
+      return { style, className };
+    },
+
+    // 获取标签默认样式
+    getDefaultTagStyle(tagName) {
+      const tagStyleMap = {
+        strong: 'font-weight: bold',
+        b: 'font-weight: bold',
+        em: 'font-style: italic',
+        i: 'font-style: italic',
+        u: 'text-decoration: underline',
+        s: 'text-decoration: line-through',
+        del: 'text-decoration: line-through',
+        sub: 'vertical-align: sub',
+        sup: 'vertical-align: super',
+      };
+      return tagStyleMap[tagName] || null;
+    },
+
+    // 创建文本块
+    createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot) {
+      const combinedStyle = tagStack
+        .filter((tagItem) => tagItem.style)
+        .map((tagItem) => tagItem.style)
+        .join(';');
+
+      const styleObj = combinedStyle ? this.parseStyleToObject(combinedStyle) : null;
+
+      return {
+        type: 'text',
+        word_list: item.word_list,
+        index: textBlockIndex,
+        oldIndex,
+        paragraphIndex,
+        styleObj,
+        hasEmphasisDot,
+      };
+    },
+
+    // 解析样式字符串为对象
+    parseStyleToObject(styleStr) {
+      const styleObj = {};
+      if (!styleStr) return styleObj;
+
+      this.mergeStyleString(styleObj, styleStr);
+      return styleObj;
+    },
+
+    // 获取单个字符的样式(包括着重点)
+    getCharStyle(word, block, charIndex) {
+      const baseStyle = { ...word.activeTextStyle };
+      baseStyle['font-size'] = baseStyle.fontSize;
+      baseStyle['font-family'] = baseStyle.fontFamily;
+
+      if (this.isAllSetting) {
+        baseStyle['font-size'] = this.fontSize;
+        baseStyle['font-family'] = this.fontFamily;
+        this.isAllSetting = false;
+      }
+
+      // 如果该文字块有着重点标记,应用到字符上
+      if (block.hasEmphasisDot) {
+        baseStyle['border-bottom'] = 'none';
+        baseStyle['background-image'] = 'radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em)';
+        baseStyle['background-size'] = '1em 0.3em';
+        baseStyle['background-repeat'] = 'repeat-x';
+        baseStyle['background-position'] = '0 100%';
+        baseStyle['padding-bottom'] = '0.3em';
+        baseStyle['display'] = 'inline';
+      }
+
+      return baseStyle;
+    },
     // 兼容历史数据
     // 兼容历史数据
     getPinyinText(item) {
     getPinyinText(item) {
       return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
       return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
@@ -391,6 +431,11 @@ export default {
       if (!this.checkShowPinyin(word.showPinyin)) {
       if (!this.checkShowPinyin(word.showPinyin)) {
         return '\u200B';
         return '\u200B';
       }
       }
+      // 优先使用新的 pinyin_list 字段(与字符一一对应)
+      if (word.pinyin_list && Array.isArray(word.pinyin_list)) {
+        return word.pinyin_list[charIndex] || '\u200B';
+      }
+      // 兼容旧数据:使用 pinyin 字段
       const pinyinList = word.pinyin ? word.pinyin.trim().split(/\s+/) : [];
       const pinyinList = word.pinyin ? word.pinyin.trim().split(/\s+/) : [];
       return pinyinList[charIndex] || '\u200B';
       return pinyinList[charIndex] || '\u200B';
     },
     },

+ 1 - 0
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -3,6 +3,7 @@
     <template #content>
     <template #content>
       <div :style="{ width: data.note_list.length > 0 ? '' : '100%' }">
       <div :style="{ width: data.note_list.length > 0 ? '' : '100%' }">
         <RichText
         <RichText
+          v-if="property.isGetContent"
           ref="richText"
           ref="richText"
           v-model="data.content"
           v-model="data.content"
           :is-view-note="true"
           :is-view-note="true"