浏览代码

Merge branch 'master' of http://60.205.254.193:3000/GCLS/eep_page

dsy 5 天之前
父节点
当前提交
c7bee2e1ae

+ 177 - 129
src/components/PinyinText.vue

@@ -33,7 +33,7 @@
                   }"
                 >
                   <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>
@@ -231,143 +231,24 @@ export default {
       let oldIndex = -1;
       let paragraphIndex = 0;
       const tagStack = [];
+
       for (const item of this.richTextList) {
         oldIndex += 1;
 
         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) {
-          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') {
-          // 处理换行符
-          blocks.push({
-            type: 'newline',
-          });
+          blocks.push({ type: 'newline' });
           paragraphIndex += 1;
         } 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;
     },
   },
@@ -380,8 +261,170 @@ export default {
       deep: true,
     },
   },
-
   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) {
+        const letterSpacing = block.styleObj?.letterSpacing || '0';
+        const letterSpacingValue = parseFloat(letterSpacing) || 0;
+
+        baseStyle['border-bottom'] = 'none';
+        baseStyle['background-image'] = 'radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em)';
+        baseStyle['background-repeat'] = 'repeat-x';
+        baseStyle['background-position'] = `${letterSpacingValue * -0.5}em 100%`;
+        baseStyle['background-size'] = `${1 + letterSpacingValue}em 0.3em`;
+        baseStyle['padding-bottom'] = '0.3em';
+        baseStyle['display'] = 'inline';
+      }
+
+      return baseStyle;
+    },
     // 兼容历史数据
     getPinyinText(item) {
       return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
@@ -391,6 +434,11 @@ export default {
       if (!this.checkShowPinyin(word.showPinyin)) {
         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+/) : [];
       return pinyinList[charIndex] || '\u200B';
     },

+ 6 - 4
src/components/RichText.vue

@@ -159,15 +159,16 @@ export default {
             line-height: 1.2 !important; /* 避免行高影响 */
           }
           .rich-text-emphasis-dot {
+           --letter-spacing-value: attr(data-letter-spacing number, 0);
             border-bottom: none;
             background-image: radial-gradient(
               circle at center,
               currentColor 0.15em,  /* 圆点大小相对于字体 */
               transparent 0.16em
             );
-            background-size: 1em 0.3em; /* 间距相对于字体大小,高度相对字体 */
+            background-size: calc((1 + var(--letter-spacing-value)) * 1em) 0.3em;
             background-repeat: repeat-x;
-            background-position: 0 100%;
+            background-position: calc(var(--letter-spacing-value) * -0.5em) 100%;
             padding-bottom: 0.3em; /* 间距也相对于字体 */
             display: inline;
           }
@@ -222,7 +223,8 @@ export default {
               const formatName = `letterSpacing${config}_em`;
               editor.formatter.register(formatName, {
                 inline: 'span',
-                styles: { 'letter-spacing': `${config}em` },
+                styles: { 'letter-spacing': `${config}em`},
+                attributes: { 'data-letter-spacing': `${config}` },
                 wrapper: true,
                 remove_similar: true,
               });
@@ -261,7 +263,7 @@ export default {
             }
             this.editorIsInited = true;
           });
-
+          
           // 自定义行高下拉(因为没有内置 lineheight 插件)
           editor.ui.registry.addMenuButton('lineheight', {
             text: '行高',

+ 7 - 4
src/styles/index.scss

@@ -95,11 +95,14 @@ ul {
 /* 富文本着重点 */
 .rich-text-emphasis-dot {
   display: inline;
-  padding-bottom: 0.2em; /* 间距也相对于字体 */
-  background-image: radial-gradient(circle at center, currentColor 0.15em, /* 圆点大小相对于字体 */ transparent 0.16em);
+  padding-bottom: 0.2em;
+
+  --letter-spacing-value: attr(data-letter-spacing number, 0);
+
+  background-image: radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em);
   background-repeat: repeat-x;
-  background-position: 0 100%;
-  background-size: 1em 0.3em; /* 间距相对于字体大小,高度相对字体 */
+  background-position: calc(var(--letter-spacing-value) * -0.5em) 100%;
+  background-size: calc((1 + var(--letter-spacing-value)) * 1em) 0.3em;
   border-bottom: none;
 }
 

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

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