|
|
@@ -33,7 +33,7 @@
|
|
|
}"
|
|
|
>
|
|
|
<span class="pinyin" :style="getPinyinStyle(word)"> {{ getCharPinyin(word, cIndex) }}</span>
|
|
|
- <span class="py-char" :style="textStyle(word)">{{ convertText(char) }}</span>
|
|
|
+ <span class="py-char" :style="getCharStyle(word, block, cIndex)">{{ convertText(char) }}</span>
|
|
|
</span>
|
|
|
</span>
|
|
|
</span>
|
|
|
@@ -231,143 +231,24 @@ export default {
|
|
|
let oldIndex = -1;
|
|
|
let paragraphIndex = 0;
|
|
|
const tagStack = [];
|
|
|
+
|
|
|
for (const item of this.richTextList) {
|
|
|
oldIndex += 1;
|
|
|
|
|
|
if (item.text && typeof item.text === 'string' && item.text.includes('<img')) {
|
|
|
- // 处理图片
|
|
|
- const imgMatch = item.text.match(/<img\s+([^>]*)\/?\s*>/i);
|
|
|
- if (imgMatch) {
|
|
|
- const attrs = imgMatch[1];
|
|
|
- const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
|
|
|
- const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
|
|
|
- const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
|
|
|
- const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
|
|
|
- const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
|
|
|
-
|
|
|
- // 获取父容器样式(从 tagStack 中累积)
|
|
|
- const containerStyleObj = {};
|
|
|
- tagStack.forEach((tagItem) => {
|
|
|
- if (tagItem.style) {
|
|
|
- tagItem.style.split(';').forEach((rule) => {
|
|
|
- if (rule.trim()) {
|
|
|
- const [prop, value] = rule.split(':').map((s) => s.trim());
|
|
|
- if (prop && value) {
|
|
|
- const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
- containerStyleObj[camelProp] = value;
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 解析图片自身样式
|
|
|
- let imageStyleObj = {};
|
|
|
- if (styleMatch) {
|
|
|
- const styleStr = styleMatch[1];
|
|
|
- styleStr.split(';').forEach((rule) => {
|
|
|
- if (rule.trim()) {
|
|
|
- const [prop, value] = rule.split(':').map((s) => s.trim());
|
|
|
- if (prop && value) {
|
|
|
- const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
- imageStyleObj[camelProp] = value;
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- blocks.push({
|
|
|
- type: 'image',
|
|
|
- src: srcMatch ? srcMatch[1] : '',
|
|
|
- alt: altMatch ? altMatch[1] : '',
|
|
|
- width: widthMatch ? widthMatch[1] : null,
|
|
|
- height: heightMatch ? heightMatch[1] : null,
|
|
|
- containerStyle: containerStyleObj,
|
|
|
- imageStyle: imageStyleObj,
|
|
|
- });
|
|
|
- }
|
|
|
+ blocks.push(this.parseImageBlock(item, tagStack));
|
|
|
} else if (item.is_style === 'true' || item.is_style === true) {
|
|
|
- const tagText = item.text || '';
|
|
|
- // 正则匹配闭标签:匹配 </tagname> 格式
|
|
|
- const closeTagMatch = tagText.match(/^<\/(\w+)>$/);
|
|
|
- if (closeTagMatch) {
|
|
|
- // 闭标签:从栈顶开始查找最近的同名标签并移除(LIFO 原则)
|
|
|
- const tagName = closeTagMatch[1];
|
|
|
- this.removeTagFromStack(tagStack, tagName);
|
|
|
- } else {
|
|
|
- // 开标签:解析标签名和样式并压栈
|
|
|
- const openTagMatch = tagText.match(/^<(\w+)([^>]*)>$/);
|
|
|
- if (openTagMatch) {
|
|
|
- const tagName = openTagMatch[1];
|
|
|
- const attrs = openTagMatch[2];
|
|
|
- // 解析 style 属性
|
|
|
- const styleMatch = attrs.match(/style=["']([^"']*)["']/);
|
|
|
- let style = styleMatch ? styleMatch[1] : null;
|
|
|
-
|
|
|
- // 将无 style 属性的标签转换为对应的 CSS 样式
|
|
|
- if (!style) {
|
|
|
- const tagStyleMap = {
|
|
|
- strong: 'font-weight: bold',
|
|
|
- b: 'font-weight: bold',
|
|
|
- em: 'font-style: italic',
|
|
|
- i: 'font-style: italic',
|
|
|
- u: 'text-decoration: underline',
|
|
|
- s: 'text-decoration: line-through',
|
|
|
- del: 'text-decoration: line-through',
|
|
|
- sub: 'vertical-align: sub',
|
|
|
- sup: 'vertical-align: super',
|
|
|
- };
|
|
|
- style = tagStyleMap[tagName] || null;
|
|
|
- }
|
|
|
-
|
|
|
- tagStack.push({
|
|
|
- tag: tagName,
|
|
|
- style,
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
+ this.handleStyleTag(item, tagStack);
|
|
|
} else if (item.text === '\n') {
|
|
|
- // 处理换行符
|
|
|
- blocks.push({
|
|
|
- type: 'newline',
|
|
|
- });
|
|
|
+ blocks.push({ type: 'newline' });
|
|
|
paragraphIndex += 1;
|
|
|
} else if (item.word_list && item.word_list.length > 0) {
|
|
|
- // 处理文字内容:将当前标签栈中的样式应用到文字块
|
|
|
- // 合并当前所有打开标签的样式
|
|
|
- let combinedStyle = '';
|
|
|
- tagStack.forEach((tagItem) => {
|
|
|
- if (tagItem.style) {
|
|
|
- combinedStyle += `${tagItem.style};`;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 将样式字符串转换为对象格式
|
|
|
- let styleObj = null;
|
|
|
- if (combinedStyle) {
|
|
|
- styleObj = {};
|
|
|
- combinedStyle.split(';').forEach((rule) => {
|
|
|
- if (rule.trim()) {
|
|
|
- const [prop, value] = rule.split(':').map((s) => s.trim());
|
|
|
- if (prop && value) {
|
|
|
- // 将驼峰命名转为 kebab-case(如 backgroundColor -> background-color)
|
|
|
- const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
- styleObj[camelProp] = value;
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- blocks.push({
|
|
|
- type: 'text',
|
|
|
- word_list: item.word_list,
|
|
|
- index: textBlockIndex++,
|
|
|
- oldIndex,
|
|
|
- paragraphIndex,
|
|
|
- styleObj, // 应用累积的样式对象
|
|
|
- });
|
|
|
+ const hasEmphasisDot = this.checkHasEmphasisDot(tagStack);
|
|
|
+ blocks.push(this.createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot));
|
|
|
+ textBlockIndex += 1;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
return blocks;
|
|
|
},
|
|
|
},
|
|
|
@@ -380,8 +261,170 @@ export default {
|
|
|
deep: true,
|
|
|
},
|
|
|
},
|
|
|
-
|
|
|
methods: {
|
|
|
+ // 检查标签栈中是否有着重点样式
|
|
|
+ checkHasEmphasisDot(tagStack) {
|
|
|
+ return tagStack.some((tagItem) => {
|
|
|
+ if (!tagItem.className) return false;
|
|
|
+ return tagItem.className.includes('rich-text-emphasis-dot');
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 解析图片块
|
|
|
+ parseImageBlock(item, tagStack) {
|
|
|
+ const imgMatch = item.text.match(/<img\s+([^>]*)\/?\s*>/i);
|
|
|
+ if (!imgMatch) return null;
|
|
|
+
|
|
|
+ const attrs = imgMatch[1];
|
|
|
+ const srcMatch = attrs.match(/src=["']([^"']*)["']/i);
|
|
|
+ const altMatch = attrs.match(/alt=["']([^"']*)["']/i);
|
|
|
+ const widthMatch = attrs.match(/width=["']?(\d+)["']?/i);
|
|
|
+ const heightMatch = attrs.match(/height=["']?(\d+)["']?/i);
|
|
|
+ const styleMatch = attrs.match(/style=["']([^"']*)["']/i);
|
|
|
+
|
|
|
+ const containerStyleObj = {};
|
|
|
+ tagStack.forEach((tagItem) => {
|
|
|
+ if (tagItem.style) {
|
|
|
+ this.mergeStyleString(containerStyleObj, tagItem.style);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const imageStyleObj = {};
|
|
|
+ if (styleMatch) {
|
|
|
+ this.mergeStyleString(imageStyleObj, styleMatch[1]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'image',
|
|
|
+ src: srcMatch ? srcMatch[1] : '',
|
|
|
+ alt: altMatch ? altMatch[1] : '',
|
|
|
+ width: widthMatch ? widthMatch[1] : null,
|
|
|
+ height: heightMatch ? heightMatch[1] : null,
|
|
|
+ containerStyle: containerStyleObj,
|
|
|
+ imageStyle: imageStyleObj,
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 合并样式字符串到对象
|
|
|
+ mergeStyleString(styleObj, styleStr) {
|
|
|
+ styleStr.split(';').forEach((rule) => {
|
|
|
+ if (rule.trim()) {
|
|
|
+ const [prop, value] = rule.split(':').map((s) => s.trim());
|
|
|
+ if (prop && value) {
|
|
|
+ const camelProp = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
+ styleObj[camelProp] = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理样式标签
|
|
|
+ handleStyleTag(item, tagStack) {
|
|
|
+ const tagText = item.text || '';
|
|
|
+ const closeTagMatch = tagText.match(/^<\/(\w+)>$/);
|
|
|
+
|
|
|
+ if (closeTagMatch) {
|
|
|
+ const tagName = closeTagMatch[1];
|
|
|
+ this.removeTagFromStack(tagStack, tagName);
|
|
|
+ } else {
|
|
|
+ const openTagMatch = tagText.match(/^<(\w+)([^>]*)>$/);
|
|
|
+ if (openTagMatch) {
|
|
|
+ const tagName = openTagMatch[1];
|
|
|
+ const attrs = openTagMatch[2];
|
|
|
+ const { style, className } = this.extractTagInfo(attrs, tagName);
|
|
|
+ tagStack.push({ tag: tagName, style, className });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 提取标签信息(样式和类名)
|
|
|
+ extractTagInfo(attrs, tagName) {
|
|
|
+ const styleMatch = attrs.match(/style=["']([^"']*)["']/);
|
|
|
+ let style = styleMatch ? styleMatch[1] : null;
|
|
|
+
|
|
|
+ const classMatch = attrs.match(/class=["']([^"']*)["']/);
|
|
|
+ const className = classMatch ? classMatch[1] : '';
|
|
|
+
|
|
|
+ if (!style) {
|
|
|
+ style = this.getDefaultTagStyle(tagName);
|
|
|
+ }
|
|
|
+
|
|
|
+ return { style, className };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取标签默认样式
|
|
|
+ getDefaultTagStyle(tagName) {
|
|
|
+ const tagStyleMap = {
|
|
|
+ strong: 'font-weight: bold',
|
|
|
+ b: 'font-weight: bold',
|
|
|
+ em: 'font-style: italic',
|
|
|
+ i: 'font-style: italic',
|
|
|
+ u: 'text-decoration: underline',
|
|
|
+ s: 'text-decoration: line-through',
|
|
|
+ del: 'text-decoration: line-through',
|
|
|
+ sub: 'vertical-align: sub',
|
|
|
+ sup: 'vertical-align: super',
|
|
|
+ };
|
|
|
+ return tagStyleMap[tagName] || null;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 创建文本块
|
|
|
+ createTextBlock(item, tagStack, textBlockIndex, oldIndex, paragraphIndex, hasEmphasisDot) {
|
|
|
+ const combinedStyle = tagStack
|
|
|
+ .filter((tagItem) => tagItem.style)
|
|
|
+ .map((tagItem) => tagItem.style)
|
|
|
+ .join(';');
|
|
|
+
|
|
|
+ const styleObj = combinedStyle ? this.parseStyleToObject(combinedStyle) : null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'text',
|
|
|
+ word_list: item.word_list,
|
|
|
+ index: textBlockIndex,
|
|
|
+ oldIndex,
|
|
|
+ paragraphIndex,
|
|
|
+ styleObj,
|
|
|
+ hasEmphasisDot,
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 解析样式字符串为对象
|
|
|
+ parseStyleToObject(styleStr) {
|
|
|
+ const styleObj = {};
|
|
|
+ if (!styleStr) return styleObj;
|
|
|
+
|
|
|
+ this.mergeStyleString(styleObj, styleStr);
|
|
|
+ return styleObj;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取单个字符的样式(包括着重点)
|
|
|
+ getCharStyle(word, block, charIndex) {
|
|
|
+ const baseStyle = { ...word.activeTextStyle };
|
|
|
+ baseStyle['font-size'] = baseStyle.fontSize;
|
|
|
+ baseStyle['font-family'] = baseStyle.fontFamily;
|
|
|
+
|
|
|
+ if (this.isAllSetting) {
|
|
|
+ baseStyle['font-size'] = this.fontSize;
|
|
|
+ baseStyle['font-family'] = this.fontFamily;
|
|
|
+ this.isAllSetting = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果该文字块有着重点标记,应用到字符上
|
|
|
+ if (block.hasEmphasisDot) {
|
|
|
+ const letterSpacing = block.styleObj?.letterSpacing || '0';
|
|
|
+ const letterSpacingValue = parseFloat(letterSpacing) || 0;
|
|
|
+
|
|
|
+ baseStyle['border-bottom'] = 'none';
|
|
|
+ baseStyle['background-image'] = 'radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em)';
|
|
|
+ baseStyle['background-repeat'] = 'repeat-x';
|
|
|
+ baseStyle['background-position'] = `${letterSpacingValue * -0.5}em 100%`;
|
|
|
+ baseStyle['background-size'] = `${1 + letterSpacingValue}em 0.3em`;
|
|
|
+ baseStyle['padding-bottom'] = '0.3em';
|
|
|
+ baseStyle['display'] = 'inline';
|
|
|
+ }
|
|
|
+
|
|
|
+ return baseStyle;
|
|
|
+ },
|
|
|
// 兼容历史数据
|
|
|
getPinyinText(item) {
|
|
|
return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
|
|
|
@@ -391,6 +434,11 @@ export default {
|
|
|
if (!this.checkShowPinyin(word.showPinyin)) {
|
|
|
return '\u200B';
|
|
|
}
|
|
|
+ // 优先使用新的 pinyin_list 字段(与字符一一对应)
|
|
|
+ if (word.pinyin_list && Array.isArray(word.pinyin_list)) {
|
|
|
+ return word.pinyin_list[charIndex] || '\u200B';
|
|
|
+ }
|
|
|
+ // 兼容旧数据:使用 pinyin 字段
|
|
|
const pinyinList = word.pinyin ? word.pinyin.trim().split(/\s+/) : [];
|
|
|
return pinyinList[charIndex] || '\u200B';
|
|
|
},
|