|
|
@@ -1,3 +1,4 @@
|
|
|
+<!-- eslint-disable vue/no-v-html -->
|
|
|
<template>
|
|
|
<div class="pinyin-area" :style="{ 'text-align': pinyinOverallPosition, padding: pinyinPadding }">
|
|
|
<AudioPlay
|
|
|
@@ -14,7 +15,7 @@
|
|
|
v-if="block.type === 'text'"
|
|
|
:key="'text-' + index"
|
|
|
class="pinyin-sentence"
|
|
|
- :style="[{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }]"
|
|
|
+ :style="[{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }, block.sentenceStyle]"
|
|
|
>
|
|
|
<span v-for="(word, wIndex) in block.word_list" :key="wIndex" class="pinyin-text" :style="block.styleObj">
|
|
|
<span
|
|
|
@@ -104,6 +105,15 @@
|
|
|
您的浏览器不支持音频播放
|
|
|
</audio>
|
|
|
</span>
|
|
|
+
|
|
|
+ <!-- 数学公式块(保留 MathJax 原始结构) -->
|
|
|
+ <MathHtml
|
|
|
+ v-else-if="block.type === 'math'"
|
|
|
+ :key="'math-' + index"
|
|
|
+ :html="block.html"
|
|
|
+ :style-obj="block.containerStyle"
|
|
|
+ :is-display="block.isDisplay"
|
|
|
+ />
|
|
|
<!-- 换行符 -->
|
|
|
<br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
|
|
|
</template>
|
|
|
@@ -176,11 +186,44 @@ import { sanitizeHTML } from '@/utils/common';
|
|
|
import { isEnable } from '@/views/book/courseware/data/common';
|
|
|
import AudioPlay from '@/views/book/courseware/preview/components/character_base/components/AudioPlay.vue';
|
|
|
|
|
|
+// MathHtml 组件用于渲染数学公式的 HTML 内容,保留 MathJax 的原始结构
|
|
|
+const MathHtml = {
|
|
|
+ name: 'MathHtml',
|
|
|
+ functional: true,
|
|
|
+ props: {
|
|
|
+ html: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+ styleObj: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({}),
|
|
|
+ },
|
|
|
+ isDisplay: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ render(h, context) {
|
|
|
+ return h('span', {
|
|
|
+ class: {
|
|
|
+ 'math-container': true,
|
|
|
+ 'math-display': context.props.isDisplay,
|
|
|
+ },
|
|
|
+ style: context.props.styleObj,
|
|
|
+ domProps: {
|
|
|
+ innerHTML: sanitizeHTML(context.props.html || ''),
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
export default {
|
|
|
name: 'PinyinText',
|
|
|
components: {
|
|
|
CorrectPinyin,
|
|
|
AudioPlay,
|
|
|
+ MathHtml,
|
|
|
},
|
|
|
inject: ['convertText'],
|
|
|
props: {
|
|
|
@@ -301,10 +344,10 @@ export default {
|
|
|
|
|
|
// 如果找到了结束标签,合并完成,跳出内部循环
|
|
|
if (nextItem.text && nextItem.text.includes('</video>')) {
|
|
|
- j++; // 跳过结束标签
|
|
|
+ j += 1; // 跳过结束标签
|
|
|
break;
|
|
|
}
|
|
|
- j++;
|
|
|
+ j += 1;
|
|
|
}
|
|
|
|
|
|
// 将合并后的内容作为一个新项加入结果
|
|
|
@@ -319,7 +362,7 @@ export default {
|
|
|
} else {
|
|
|
// 其他项直接加入
|
|
|
result.push(item);
|
|
|
- i++;
|
|
|
+ i += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -335,10 +378,33 @@ export default {
|
|
|
let oldIndex = -1;
|
|
|
let paragraphIndex = 0;
|
|
|
const tagStack = [];
|
|
|
+ let mathContext = null;
|
|
|
|
|
|
for (const item of listToParse) {
|
|
|
oldIndex += 1;
|
|
|
|
|
|
+ if (mathContext) {
|
|
|
+ mathContext.html += this.getRawItemText(item);
|
|
|
+
|
|
|
+ if (item.is_style === 'true' || item.is_style === true) {
|
|
|
+ this.updateHtmlTagStack(mathContext.stack, item.text || '');
|
|
|
+ if (mathContext.stack.length === 0) {
|
|
|
+ blocks.push(this.parseMathBlock(mathContext.html, tagStack));
|
|
|
+ mathContext = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.isMathRootTag(item)) {
|
|
|
+ const rootTag = this.extractTagName(item.text || '');
|
|
|
+ mathContext = {
|
|
|
+ html: this.getRawItemText(item),
|
|
|
+ stack: rootTag ? [rootTag] : ['span'],
|
|
|
+ };
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
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')) {
|
|
|
@@ -363,6 +429,10 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ if (mathContext) {
|
|
|
+ blocks.push(this.parseMathBlock(mathContext.html, tagStack));
|
|
|
+ }
|
|
|
+
|
|
|
return blocks;
|
|
|
},
|
|
|
},
|
|
|
@@ -607,6 +677,13 @@ export default {
|
|
|
|
|
|
const styleObj = combinedStyle ? this.parseStyleToObject(combinedStyle) : null;
|
|
|
|
|
|
+ // 提取 paddingLeft 用于段落缩进,从 styleObj 中移除
|
|
|
+ const sentenceStyle = {};
|
|
|
+ if (styleObj && styleObj.paddingLeft) {
|
|
|
+ sentenceStyle.paddingLeft = styleObj.paddingLeft;
|
|
|
+ delete styleObj.paddingLeft;
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
type: 'text',
|
|
|
word_list: item.word_list,
|
|
|
@@ -614,6 +691,7 @@ export default {
|
|
|
oldIndex,
|
|
|
paragraphIndex,
|
|
|
styleObj,
|
|
|
+ sentenceStyle,
|
|
|
rawStyleText: combinedStyle,
|
|
|
hasEmphasisDot,
|
|
|
};
|
|
|
@@ -628,6 +706,59 @@ export default {
|
|
|
return styleObj;
|
|
|
},
|
|
|
|
|
|
+ parseMathBlock(html, tagStack) {
|
|
|
+ const containerStyleObj = {};
|
|
|
+ tagStack.forEach((tagItem) => {
|
|
|
+ if (tagItem.style) {
|
|
|
+ this.mergeStyleString(containerStyleObj, tagItem.style);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const isDisplay =
|
|
|
+ /<mjx-container[^>]*\bdisplay=["']true["']/i.test(html) ||
|
|
|
+ /<mjx-assistive-mml[^>]*\bdisplay=["']block["']/i.test(html);
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'math',
|
|
|
+ html,
|
|
|
+ containerStyle: containerStyleObj,
|
|
|
+ isDisplay,
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ isMathRootTag(item) {
|
|
|
+ if (!item || !(item.is_style === 'true' || item.is_style === true) || !item.text) return false;
|
|
|
+ return /class=["'][^"']*\bmathjax-container\b[^"']*\beditor-math\b[^"']*["']/i.test(item.text);
|
|
|
+ },
|
|
|
+
|
|
|
+ getRawItemText(item) {
|
|
|
+ if (!item) return '';
|
|
|
+ return item.text || '';
|
|
|
+ },
|
|
|
+
|
|
|
+ extractTagName(tagText) {
|
|
|
+ const match = (tagText || '').match(/^<\/?\s*([a-zA-Z][\w-]*)/);
|
|
|
+ return match ? match[1].toLowerCase() : '';
|
|
|
+ },
|
|
|
+
|
|
|
+ updateHtmlTagStack(tagStack, tagText) {
|
|
|
+ if (!tagText || !tagStack) return;
|
|
|
+
|
|
|
+ const tagName = this.extractTagName(tagText);
|
|
|
+ if (!tagName) return;
|
|
|
+
|
|
|
+ if (/^<\//.test(tagText)) {
|
|
|
+ for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
|
+ if (tagStack[i] === tagName) {
|
|
|
+ tagStack.splice(i, 1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (!/\/>\s*$/.test(tagText)) {
|
|
|
+ tagStack.push(tagName);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
getBlockColor(block) {
|
|
|
const colorFromObject = block?.styleObj?.color;
|
|
|
if (colorFromObject) return colorFromObject;
|
|
|
@@ -726,6 +857,32 @@ export default {
|
|
|
this.isAllSetting = false;
|
|
|
}
|
|
|
|
|
|
+ // 优先取文字单独的字号
|
|
|
+ let _fontSize = block.styleObj && block.styleObj.fontSize ? block.styleObj.fontSize : null;
|
|
|
+ // 如果没有,取继承的字号
|
|
|
+ if (!_fontSize && item.activeTextStyle && item.activeTextStyle.fontSize) {
|
|
|
+ _fontSize = item.activeTextStyle.fontSize;
|
|
|
+ }
|
|
|
+ // 如果没有,取组件的字号
|
|
|
+ if (!_fontSize && this.bodyStyles.fontSize) _fontSize = this.bodyStyles.fontSize;
|
|
|
+ // 如果没有,取全局字号
|
|
|
+ if (!_fontSize && this.fontSize) _fontSize = this.fontSize;
|
|
|
+
|
|
|
+ if (_fontSize) {
|
|
|
+ // 取出字号里面的数字,做计算处理之后,自动加上原来的单位
|
|
|
+ const fontSizeMatch = _fontSize.match(/^([\d.]+)(\D*)$/);
|
|
|
+ if (fontSizeMatch) {
|
|
|
+ const fontSizeValue = parseFloat(fontSizeMatch[1]);
|
|
|
+ const fontSizeUnit = fontSizeMatch[2] || 'px';
|
|
|
+
|
|
|
+ // 根据字号大小确定比例:16号及以下75%,16号以上60%
|
|
|
+ const ratio = fontSizeValue <= 16 ? 0.75 : 0.6;
|
|
|
+ const calculatedSize = Math.round(fontSizeValue * ratio);
|
|
|
+
|
|
|
+ // 保留原始单位
|
|
|
+ styles['font-size'] = `${calculatedSize}${fontSizeUnit}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
// 明确重置不应该继承的样式
|
|
|
styles['text-decoration'] = 'none';
|
|
|
styles['font-style'] = 'normal';
|
|
|
@@ -793,6 +950,22 @@ export default {
|
|
|
margin: 0.5em 0;
|
|
|
}
|
|
|
|
|
|
+ :deep(.mathjax-container.editor-math > svg[aria-hidden='true']) {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .math-container.math-display {
|
|
|
+ display: block !important;
|
|
|
+ width: 100%;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .math-container.math-display :deep(mjx-container[display='true']) {
|
|
|
+ display: block !important;
|
|
|
+ margin: 0 auto;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
span {
|
|
|
display: inline-flex;
|
|
|
}
|