Browse Source

富文本优化

zq 3 days ago
parent
commit
863e4dbfa4
1 changed files with 208 additions and 13 deletions
  1. 208 13
      src/components/PinyinText.vue

+ 208 - 13
src/components/PinyinText.vue

@@ -53,7 +53,7 @@
           </span>
         </span>
         <!-- 图片块 -->
-        <div
+        <span
           v-else-if="block.type === 'image'"
           :key="'image-' + index"
           :style="block.containerStyle"
@@ -67,7 +67,39 @@
             :style="block.imageStyle"
             class="inline-image"
           />
-        </div>
+        </span>
+
+        <!-- 视频块 -->
+        <span
+          v-else-if="block.type === 'video'"
+          :key="'video-' + index"
+          :style="block.containerStyle"
+          class="video-container"
+        >
+          <video
+            :poster="block.poster"
+            :width="block.width"
+            :height="block.height"
+            :style="block.mediaStyle"
+            controls
+            class="inline-video"
+          >
+            <source :src="block.src" :type="block.videoType" />
+            您的浏览器不支持视频播放
+          </video>
+        </span>
+
+        <!-- 音频块 -->
+        <span
+          v-else-if="block.type === 'audio'"
+          :key="'audio-' + index"
+          :style="block.containerStyle"
+          class="audio-container"
+        >
+          <audio :src="block.src" :style="block.mediaStyle" controls class="inline-audio">
+            您的浏览器不支持音频播放
+          </audio>
+        </span>
         <!-- 换行符 -->
         <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
       </template>
@@ -228,22 +260,87 @@ export default {
         fontFamily: this.fontFamily,
       };
     },
+
+    // 预处理 richTextList,合并分散的视频标签
+    normalizedRichTextList() {
+      if (!this.richTextList || this.richTextList.length === 0) return [];
+
+      const result = [];
+      let i = 0;
+      while (i < this.richTextList.length) {
+        const item = this.richTextList[i];
+
+        // 检查是否是视频开始标签(且不包含结束标签)
+        if (
+          item.text &&
+          typeof item.text === 'string' &&
+          item.text.includes('<video') &&
+          !item.text.includes('</video>')
+        ) {
+          let combinedText = item.text;
+          let j = i + 1;
+
+          // 向后查找 source 和结束标签,直到遇到 </video> 或超出范围
+          while (j < this.richTextList.length) {
+            const nextItem = this.richTextList[j];
+            if (nextItem.text) {
+              combinedText += `${nextItem.text}`;
+            }
+
+            // 如果找到了结束标签,合并完成,跳出内部循环
+            if (nextItem.text && nextItem.text.includes('</video>')) {
+              j++; // 跳过结束标签
+              break;
+            }
+            j++;
+          }
+
+          // 将合并后的内容作为一个新项加入结果
+          result.push({
+            ...item,
+            text: combinedText,
+            is_merged_video: true, // 标记一下,可选
+          });
+
+          // 更新主循环索引,跳过已合并的项
+          i = j;
+        } else {
+          // 其他项直接加入
+          result.push(item);
+          i++;
+        }
+      }
+
+      return result;
+    },
     // 解析 richTextList 为可渲染的块
     parsedBlocks() {
-      if (!this.richTextList || this.richTextList.length === 0) return [];
+      const listToParse = this.normalizedRichTextList;
+
+      if (!listToParse || listToParse.length === 0) return [];
       const blocks = [];
       let textBlockIndex = 0;
       let oldIndex = -1;
       let paragraphIndex = 0;
       const tagStack = [];
 
-      for (const item of this.richTextList) {
+      for (const item of listToParse) {
         oldIndex += 1;
 
         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')) {
+          // item.text 是完整的 video 标签串
+          blocks.push(this.parseVideoBlock(item, tagStack));
+        } else if (item.text && typeof item.text === 'string' && item.text.includes('<audio')) {
+          blocks.push(this.parseAudioBlock(item, tagStack));
         } else if (item.is_style === 'true' || item.is_style === true) {
-          this.handleStyleTag(item, tagStack);
+          if (item.text.includes('<br')) {
+            blocks.push({ type: 'newline' });
+            paragraphIndex += 1;
+          } else {
+            this.handleStyleTag(item, tagStack);
+          }
         } else if (item.text === '\n') {
           blocks.push({ type: 'newline' });
           paragraphIndex += 1;
@@ -348,6 +445,84 @@ export default {
       };
     },
 
+    // 解析视频块
+    parseVideoBlock(item, tagStack) {
+      const videoMatch = item.text.match(/<video\s+([^>]*)>/i);
+      if (!videoMatch) return null;
+
+      const attrs = videoMatch[1];
+      const posterMatch = attrs.match(/poster=["']([^"']*)["']/i);
+      const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
+      const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
+      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+
+      // 直接从完整的文本中匹配 source
+      const sourceMatch = item.text.match(/<source\s+([^>]*)\/?\s*>/i);
+      let src = '';
+      let videoType = '';
+
+      if (sourceMatch) {
+        const sourceAttrs = sourceMatch[1];
+        const srcMatch = sourceAttrs.match(/src\s*=\s*["']?([^"'\s>]+)["']?/i);
+        const typeMatch = sourceAttrs.match(/type\s*=\s*["']?([^"'\s>]+)["']?/i);
+
+        src = srcMatch ? srcMatch[1] : '';
+        videoType = typeMatch ? typeMatch[1] : '';
+      }
+
+      const containerStyleObj = {};
+      tagStack.forEach((tagItem) => {
+        if (tagItem.style) {
+          this.mergeStyleString(containerStyleObj, tagItem.style);
+        }
+      });
+
+      const mediaStyleObj = {};
+      if (styleMatch) {
+        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      }
+
+      return {
+        type: 'video',
+        poster: posterMatch ? posterMatch[1] : '',
+        width: widthMatch ? widthMatch[1] : null,
+        height: heightMatch ? heightMatch[1] : null,
+        src,
+        videoType,
+        containerStyle: containerStyleObj,
+        mediaStyle: mediaStyleObj,
+      };
+    },
+
+    // 解析音频块
+    parseAudioBlock(item, tagStack) {
+      const audioMatch = item.text.match(/<audio\s+([^>]*)>/i);
+      if (!audioMatch) return null;
+
+      const attrs = audioMatch[1];
+      const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
+      const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
+
+      const containerStyleObj = {};
+      tagStack.forEach((tagItem) => {
+        if (tagItem.style) {
+          this.mergeStyleString(containerStyleObj, tagItem.style);
+        }
+      });
+
+      const mediaStyleObj = {};
+      if (styleMatch) {
+        this.mergeStyleString(mediaStyleObj, styleMatch[1]);
+      }
+
+      return {
+        type: 'audio',
+        src: srcMatch ? srcMatch[1] : '',
+        containerStyle: containerStyleObj,
+        mediaStyle: mediaStyleObj,
+      };
+    },
+
     // 合并样式字符串到对象
     mergeStyleString(styleObj, styleStr) {
       styleStr.split(';').forEach((rule) => {
@@ -441,7 +616,7 @@ export default {
     },
 
     // 获取单个字符的样式(包括着重点)
-    getCharStyle(word, block, charIndex) {
+    getCharStyle(word, block) {
       const baseStyle = { ...word.activeTextStyle };
       baseStyle['font-size'] = baseStyle.fontSize;
       baseStyle['font-family'] = baseStyle.fontFamily;
@@ -582,12 +757,12 @@ export default {
     }
 
     .pinyin-sentence {
-      display: inline-flex;
-      flex-wrap: wrap;
+      display: inline;
+      white-space: normal;
     }
 
     .image-container {
-      display: block;
+      display: inline-block;
 
       .inline-image {
         display: inline-block;
@@ -595,19 +770,39 @@ export default {
         height: auto;
       }
     }
+
+    .video-container {
+      display: inline-block;
+      margin: 26px 5px 26px 0;
+
+      .inline-video {
+        display: inline-block;
+        max-width: 100%;
+        height: auto;
+      }
+    }
+
+    .audio-container {
+      display: block;
+
+      .inline-audio {
+        display: inline-block;
+        width: 100%;
+        max-width: 500px;
+      }
+    }
   }
 
   .pinyin-paragraph {
     .pinyin-sentence {
-      display: inline-flex;
-      flex-wrap: wrap;
+      display: inline;
+      white-space: normal;
     }
   }
 
   .pinyin-text {
+    display: inline-flex;
     padding: 0 2px;
-
-    // font-size: 16px;
     text-wrap: pretty;
     hanging-punctuation: allow-end;