Sfoglia il codice sorgente

表格预览添加音视频显示

natasha 1 settimana fa
parent
commit
0ee7977726

+ 235 - 6
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -58,7 +58,10 @@
                           v-for="(item, index) in col.rich_text_list"
                           :key="index"
                           class="pinyin-text"
-                          :class="[index === 0 ? 'pinyin-text-left' : '']"
+                          :class="[
+                            index === 0 ? 'pinyin-text-left' : '',
+                            item.type === 'audio' ? 'pinyin-text-audio' : '',
+                          ]"
                         >
                           <!-- 文字块:包含 word_list -->
                           <span
@@ -167,8 +170,38 @@
                               class="inline-image"
                             />
                           </div>
+                          <!-- 视频块 -->
+                          <div
+                            v-else-if="item.type === 'video'"
+                            :key="'video-' + index"
+                            :style="item.containerStyle"
+                            class="video-container"
+                          >
+                            <video
+                              :poster="item.poster"
+                              :width="item.width"
+                              :height="item.height"
+                              :style="item.mediaStyle"
+                              controls
+                              class="inline-video"
+                            >
+                              <source :src="item.src" :type="item.videoType" />
+                              您的浏览器不支持视频播放
+                            </video>
+                          </div>
+                          <!-- 音频块 -->
+                          <div
+                            v-else-if="item.type === 'audio'"
+                            :key="'audio-' + index"
+                            :style="item.containerStyle"
+                            class="audio-container"
+                          >
+                            <audio :src="item.src" :style="item.mediaStyle" controls class="inline-audio">
+                              您的浏览器不支持音频播放
+                            </audio>
+                          </div>
                           <!-- 换行符 -->
-                          <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
+                          <br v-else-if="item.type === 'newline'" :key="'newline-' + index" />
 
                           <!-- 
                         <template v-else>
@@ -355,7 +388,10 @@
                             v-for="(item, index) in col.rich_text_list"
                             :key="index"
                             class="pinyin-text"
-                            :class="[index === 0 ? 'pinyin-text-left' : '']"
+                            :class="[
+                              index === 0 ? 'pinyin-text-left' : '',
+                              item.type === 'audio' ? 'pinyin-text-audio' : '',
+                            ]"
                           >
                             <!-- 文字块:包含 word_list -->
                             <span
@@ -415,8 +451,39 @@
                                 class="inline-image"
                               />
                             </div>
+                            <!-- 视频块 -->
+                            <div
+                              v-else-if="item.type === 'video'"
+                              :key="'video-' + index"
+                              :style="item.containerStyle"
+                              class="video-container"
+                            >
+                              <video
+                                :poster="item.poster"
+                                :width="item.width"
+                                :height="item.height"
+                                :style="item.mediaStyle"
+                                controls
+                                class="inline-video"
+                              >
+                                <source :src="item.src" :type="item.videoType" />
+                                您的浏览器不支持视频播放
+                              </video>
+                            </div>
+
+                            <!-- 音频块 -->
+                            <div
+                              v-else-if="item.type === 'audio'"
+                              :key="'audio-' + index"
+                              :style="item.containerStyle"
+                              class="audio-container"
+                            >
+                              <audio :src="item.src" :style="item.mediaStyle" controls class="inline-audio">
+                                您的浏览器不支持音频播放
+                              </audio>
+                            </div>
                             <!-- 换行符 -->
-                            <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
+                            <br v-else-if="item.type === 'newline'" :key="'newline-' + index" />
                           </div>
                         </template>
                       </template>
@@ -612,20 +679,80 @@ export default {
     this.computedTableCellShow();
   },
   methods: {
+    // 预处理 richTextList,合并分散的视频标签
+    normalizedRichTextList(col) {
+      if (!col.rich_text_list || col.rich_text_list.length === 0) return [];
+
+      const result = [];
+      let i = 0;
+      while (i < col.rich_text_list.length) {
+        const item = col.rich_text_list[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 < col.rich_text_list.length) {
+            const nextItem = col.rich_text_list[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(col) {
-      if (!col.rich_text_list || col.rich_text_list.length === 0) return [];
+      const listToParse = this.normalizedRichTextList(col);
+
+      if (!listToParse || listToParse.length === 0) return [];
+      // if (!col.rich_text_list || col.rich_text_list.length === 0) return [];
       const blocks = [];
       let textBlockIndex = 0;
       let oldIndex = -1;
       let paragraphIndex = 0;
       const tagStack = [];
 
-      for (const item of col.rich_text_list) {
+      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);
         } else if (item.text === '\n') {
@@ -820,6 +947,83 @@ export default {
       this.mergeStyleString(styleObj, styleStr);
       return styleObj;
     },
+    // 解析视频块
+    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) => {
@@ -1269,6 +1473,31 @@ $border-color: #e6e6e6;
           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: inline-block;
+
+        .inline-audio {
+          display: inline-block;
+          width: 100%;
+          max-width: 500px;
+        }
+      }
+
+      .pinyin-text-audio {
+        width: 100%;
+      }
     }
 
     :deep .el-input--small .el-input__inner {