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