|
|
@@ -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 {
|