|
|
@@ -1,44 +1,92 @@
|
|
|
<template>
|
|
|
<div class="pinyin-area" :style="{ 'text-align': pinyinOverallPosition, padding: pinyinPadding }">
|
|
|
- <div v-for="(paragraph, i) in paragraphList" :key="i" class="pinyin-paragraph">
|
|
|
- <span
|
|
|
- v-for="(sentence, j) in paragraph"
|
|
|
- :key="j"
|
|
|
- class="pinyin-sentence"
|
|
|
- :style="{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }"
|
|
|
- >
|
|
|
- <span v-for="(item, k) in sentence" :key="k" class="pinyin-text">
|
|
|
- <span
|
|
|
- :class="{ active: visible && word_index == k && paragraph_index === i && sentence_index === j }"
|
|
|
- :title="isPreview ? '' : '点击校对'"
|
|
|
- :style="{
|
|
|
- cursor: isPreview ? '' : 'pointer',
|
|
|
- 'align-items': k == 0 ? 'flex-start' : 'center',
|
|
|
- }"
|
|
|
- @click="correctPinyin(item, i, j, k)"
|
|
|
- >
|
|
|
+ <!-- 新规则:使用 richTextList -->
|
|
|
+ <div v-if="richTextList && richTextList.length > 0" class="rich-text-container">
|
|
|
+ <template v-for="(block, index) in parsedBlocks">
|
|
|
+ <!-- 文字块:包含 word_list -->
|
|
|
+ <span
|
|
|
+ v-if="block.type === 'text'"
|
|
|
+ :key="'text-' + index"
|
|
|
+ class="pinyin-sentence"
|
|
|
+ :style="[{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }, block.styleObj]"
|
|
|
+ >
|
|
|
+ <span v-for="(word, wIndex) in block.word_list" :key="wIndex" class="pinyin-text">
|
|
|
<span
|
|
|
- v-if="pinyinPosition === 'top' && hasPinyinInParagraph(i)"
|
|
|
- class="pinyin"
|
|
|
- :style="{ 'font-size': pinyinSize }"
|
|
|
- >
|
|
|
- {{ getPinyinText(item) }}</span
|
|
|
- >
|
|
|
- <span class="py-char" :style="textStyle(item)">{{ convertText(item.text) }}</span>
|
|
|
- <span
|
|
|
- v-if="pinyinPosition !== 'top' && hasPinyinInParagraph(i)"
|
|
|
- class="pinyin"
|
|
|
- :style="{ 'font-size': pinyinSize }"
|
|
|
- >
|
|
|
- {{ getPinyinText(item) }}</span
|
|
|
+ :class="{ active: visible && word_index == wIndex && sentence_index === block.index }"
|
|
|
+ :title="isPreview ? '' : '点击校对'"
|
|
|
+ :style="{
|
|
|
+ cursor: isPreview ? '' : 'pointer',
|
|
|
+ 'align-items': wIndex == 0 ? 'flex-start' : 'center',
|
|
|
+ }"
|
|
|
+ @click="correctPinyin(word, block.paragraphIndex, block.oldIndex, wIndex)"
|
|
|
>
|
|
|
+ <span
|
|
|
+ v-if="pinyinPosition === 'top' && hasPinyinInParagraphNeVersion(block.paragraphIndex)"
|
|
|
+ class="pinyin"
|
|
|
+ >
|
|
|
+ {{ getPinyinText(word) }}</span
|
|
|
+ >
|
|
|
+ <span class="py-char" :style="textStyle(word)">{{ convertText(word.text) }}</span>
|
|
|
+ <span
|
|
|
+ v-if="pinyinPosition !== 'top' && hasPinyinInParagraphNeVersion(block.paragraphIndex)"
|
|
|
+ class="pinyin"
|
|
|
+ >
|
|
|
+ {{ getPinyinText(word) }}</span
|
|
|
+ >
|
|
|
+ </span>
|
|
|
</span>
|
|
|
</span>
|
|
|
- </span>
|
|
|
+
|
|
|
+ <!-- 换行符 -->
|
|
|
+ <br v-else-if="block.type === 'newline'" :key="'newline-' + index" />
|
|
|
+ </template>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 老规则:使用 paragraphList -->
|
|
|
+ <template v-else-if="paragraphList && paragraphList.length > 0">
|
|
|
+ <div v-for="(paragraph, pIndex) in paragraphList" :key="pIndex" class="pinyin-paragraph">
|
|
|
+ <div
|
|
|
+ v-for="(sentence, sIndex) in paragraph"
|
|
|
+ :key="sIndex"
|
|
|
+ class="pinyin-sentence"
|
|
|
+ :style="{ 'align-items': pinyinPosition === 'top' ? 'flex-end' : 'flex-start' }"
|
|
|
+ >
|
|
|
+ <span v-for="(word, wIndex) in sentence" :key="wIndex" class="pinyin-text">
|
|
|
+ <span
|
|
|
+ :class="{
|
|
|
+ active: visible && word_index == wIndex && sentence_index === sIndex && paragraph_index === pIndex,
|
|
|
+ }"
|
|
|
+ :title="isPreview ? '' : '点击校对'"
|
|
|
+ :style="{
|
|
|
+ cursor: isPreview ? '' : 'pointer',
|
|
|
+ 'align-items': wIndex == 0 ? 'flex-start' : 'center',
|
|
|
+ }"
|
|
|
+ @click="correctPinyin(word, pIndex, sIndex, wIndex)"
|
|
|
+ >
|
|
|
+ <span
|
|
|
+ v-if="pinyinPosition === 'top' && hasPinyinInParagraph(sIndex)"
|
|
|
+ class="pinyin"
|
|
|
+ :style="{ 'font-size': pinyinSize }"
|
|
|
+ >
|
|
|
+ {{ getPinyinText(word) }}</span
|
|
|
+ >
|
|
|
+ <span class="py-char" :style="textStyle(word)">{{ convertText(word.text) }}</span>
|
|
|
+ <span
|
|
|
+ v-if="pinyinPosition !== 'top' && hasPinyinInParagraph(sIndex)"
|
|
|
+ class="pinyin"
|
|
|
+ :style="{ 'font-size': pinyinSize }"
|
|
|
+ >
|
|
|
+ {{ getPinyinText(word) }}</span
|
|
|
+ >
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
<CorrectPinyin
|
|
|
:visible.sync="visible"
|
|
|
+ :new-version="newVersion"
|
|
|
:select-content="selectContent"
|
|
|
:component-type="componentType"
|
|
|
@fillTonePinyin="fillTonePinyin"
|
|
|
@@ -73,7 +121,11 @@ export default {
|
|
|
props: {
|
|
|
paragraphList: {
|
|
|
type: Array,
|
|
|
- required: true,
|
|
|
+ required: false,
|
|
|
+ },
|
|
|
+ richTextList: {
|
|
|
+ type: Array,
|
|
|
+ required: false,
|
|
|
},
|
|
|
pinyinPosition: {
|
|
|
type: String,
|
|
|
@@ -89,7 +141,6 @@ export default {
|
|
|
},
|
|
|
pinyinSize: {
|
|
|
type: String,
|
|
|
- default: '12pt',
|
|
|
},
|
|
|
pinyinOverallPosition: {
|
|
|
type: String,
|
|
|
@@ -129,12 +180,118 @@ export default {
|
|
|
};
|
|
|
},
|
|
|
computed: {
|
|
|
+ newVersion() {
|
|
|
+ return this.richTextList && this.richTextList.length > 0;
|
|
|
+ },
|
|
|
styleWatch() {
|
|
|
return {
|
|
|
fontSize: this.fontSize,
|
|
|
fontFamily: this.fontFamily,
|
|
|
};
|
|
|
},
|
|
|
+ // 解析 richTextList 为可渲染的块
|
|
|
+ parsedBlocks() {
|
|
|
+ if (!this.richTextList || this.richTextList.length === 0) return [];
|
|
|
+
|
|
|
+ const blocks = [];
|
|
|
+ let textBlockIndex = 0;
|
|
|
+ let oldIndex = -1;
|
|
|
+ let paragraphIndex = 0;
|
|
|
+ const tagStack = [];
|
|
|
+ for (const item of this.richTextList) {
|
|
|
+ oldIndex++;
|
|
|
+
|
|
|
+ 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];
|
|
|
+ for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
|
+ if (tagStack[i].tag === tagName) {
|
|
|
+ tagStack.splice(i, 1);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } 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: style,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 处理换行符
|
|
|
+ else if (item.text === '\n') {
|
|
|
+ blocks.push({
|
|
|
+ type: 'newline',
|
|
|
+ });
|
|
|
+ paragraphIndex++;
|
|
|
+ }
|
|
|
+ // 处理文字内容:将当前标签栈中的样式应用到文字块
|
|
|
+ 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: oldIndex,
|
|
|
+ paragraphIndex: paragraphIndex,
|
|
|
+ styleObj: styleObj, // 应用累积的样式对象
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return blocks;
|
|
|
+ },
|
|
|
},
|
|
|
watch: {
|
|
|
styleWatch: {
|
|
|
@@ -144,6 +301,13 @@ export default {
|
|
|
immediate: true,
|
|
|
deep: true,
|
|
|
},
|
|
|
+ richTextList: {
|
|
|
+ handler(val) {
|
|
|
+ console.log('richTextList', JSON.stringify(val));
|
|
|
+ },
|
|
|
+ immediate: true,
|
|
|
+ deep: true,
|
|
|
+ },
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
@@ -152,15 +316,15 @@ export default {
|
|
|
styles['font-size'] = styles.fontSize;
|
|
|
styles['font-family'] = styles.fontFamily;
|
|
|
|
|
|
- // if (this.fontSize) styles['font-size'] = this.fontSize;
|
|
|
- // if (this.fontFamily) styles['font-family'] = this.fontFamily;
|
|
|
- if (this.isAllSetting || !styles.fontSize) styles['font-size'] = this.fontSize;
|
|
|
- if (this.isAllSetting || !styles.fontFamily) styles['font-family'] = this.fontFamily;
|
|
|
+ if (this.isAllSetting) styles['font-size'] = this.fontSize;
|
|
|
+ if (this.isAllSetting) styles['font-family'] = this.fontFamily;
|
|
|
this.isAllSetting = false;
|
|
|
return styles;
|
|
|
},
|
|
|
- // 校对拼音
|
|
|
- correctPinyin(item, i, j, k) {
|
|
|
+ /**
|
|
|
+ * 校对拼音
|
|
|
+ */
|
|
|
+ correctPinyin(item, pIndex, sIndex, wIndex) {
|
|
|
if (this.isPreview) {
|
|
|
if (item.note) {
|
|
|
this.note = item.note;
|
|
|
@@ -168,11 +332,11 @@ export default {
|
|
|
this.noteDialogVisible = true;
|
|
|
this.$nextTick(() => {
|
|
|
const dialogElement = this.$refs.optimizedDialog;
|
|
|
- // 确保对话框DOM已渲染
|
|
|
+ // 确保对话框 DOM 已渲染
|
|
|
if (!dialogElement) {
|
|
|
return;
|
|
|
}
|
|
|
- // 获取对话框内容区域的DOM元素
|
|
|
+ // 获取对话框内容区域的 DOM 元素
|
|
|
const dialogContent = dialogElement.$el.querySelector('.el-dialog');
|
|
|
if (!dialogContent) {
|
|
|
return;
|
|
|
@@ -218,9 +382,9 @@ export default {
|
|
|
if (item) {
|
|
|
this.visible = true;
|
|
|
this.selectContent = item;
|
|
|
- this.paragraph_index = i;
|
|
|
- this.sentence_index = j;
|
|
|
- this.word_index = k;
|
|
|
+ this.paragraph_index = pIndex;
|
|
|
+ this.sentence_index = sIndex;
|
|
|
+ this.word_index = wIndex;
|
|
|
}
|
|
|
},
|
|
|
// 回填校对后的拼音
|
|
|
@@ -230,6 +394,7 @@ export default {
|
|
|
i: this.paragraph_index,
|
|
|
j: this.sentence_index,
|
|
|
k: this.word_index,
|
|
|
+ newVersion: this.newVersion,
|
|
|
});
|
|
|
},
|
|
|
// 如段落有拼音,则返回true ,否则不显示拼音行
|
|
|
@@ -237,7 +402,12 @@ export default {
|
|
|
const paragraph = this.paragraphList[paragraphIndex];
|
|
|
return paragraph.some((sentence) => sentence.some((item) => this.checkShowPinyin(item.showPinyin)));
|
|
|
},
|
|
|
- // 兼容历史数据,没有showPinyin字段的,则默认为true
|
|
|
+ hasPinyinInParagraphNeVersion(paragraphIndex) {
|
|
|
+ const paragraphs = this.parsedBlocks.filter((item) => item.paragraphIndex === paragraphIndex);
|
|
|
+
|
|
|
+ return paragraphs.flatMap((item) => item.word_list).some((item) => this.checkShowPinyin(item.showPinyin));
|
|
|
+ },
|
|
|
+ // 兼容历史数据,没有 showPinyin 字段的,则默认为 true
|
|
|
checkShowPinyin(showPinyin) {
|
|
|
if (showPinyin === undefined || showPinyin === null) {
|
|
|
return true;
|
|
|
@@ -245,7 +415,7 @@ export default {
|
|
|
return this.isEnable(showPinyin);
|
|
|
},
|
|
|
getPinyinText(item) {
|
|
|
- return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '';
|
|
|
+ return this.checkShowPinyin(item.showPinyin) ? item.pinyin.replace(/\s+/g, '') : '\u200B';
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
@@ -253,34 +423,59 @@ export default {
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.pinyin-area {
|
|
|
+ // 新规则的容器样式
|
|
|
+ .rich-text-container {
|
|
|
+ p {
|
|
|
+ display: inline-flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin: 0.5em 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: inline-flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pinyin-sentence {
|
|
|
+ display: inline-flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.pinyin-paragraph {
|
|
|
.pinyin-sentence {
|
|
|
- .pinyin-text {
|
|
|
- padding: 0 2px;
|
|
|
- font-size: 16px;
|
|
|
- text-wrap: pretty;
|
|
|
- hanging-punctuation: allow-end;
|
|
|
-
|
|
|
- > span {
|
|
|
- display: inline-flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
+ display: inline-flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- .py-char {
|
|
|
- ruby-align: center;
|
|
|
- }
|
|
|
+ .pinyin-text {
|
|
|
+ padding: 0 2px;
|
|
|
|
|
|
- .pinyin {
|
|
|
- display: inline-block;
|
|
|
- min-height: 12px;
|
|
|
- font-family: 'PINYIN-B';
|
|
|
- font-size: 12px;
|
|
|
- font-weight: lighter;
|
|
|
- line-height: 12px;
|
|
|
- color: $font-color;
|
|
|
- }
|
|
|
- }
|
|
|
+ // font-size: 16px;
|
|
|
+ text-wrap: pretty;
|
|
|
+ hanging-punctuation: allow-end;
|
|
|
+
|
|
|
+ > span {
|
|
|
+ display: inline-flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .py-char {
|
|
|
+ ruby-align: center;
|
|
|
+ line-height: 1em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pinyin {
|
|
|
+ display: inline-block;
|
|
|
+ min-height: 12px;
|
|
|
+
|
|
|
+ // font-family: 'PINYIN-B';
|
|
|
+ // font-size: 12px;
|
|
|
+ // font-weight: lighter;
|
|
|
+ // line-height: 12px;
|
|
|
+ // color: $font-color;
|
|
|
+ line-height: 1em;
|
|
|
}
|
|
|
}
|
|
|
|