Browse Source

富文本转拼音初版

zq 2 weeks ago
parent
commit
fa053529c1

+ 265 - 70
src/components/PinyinText.vue

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

+ 8 - 2
src/views/book/courseware/create/components/base/common/CorrectPinyin.vue

@@ -9,7 +9,7 @@
     :append-to-body="true"
     @close="dialogClose"
   >
-    <div ref="toolbarMenu" class="toolbar">
+    <div ref="toolbarMenu" v-if="!newVersion" class="toolbar">
       <el-select v-model="dataContent.activeTextStyle.fontFamily" placeholder="请选择">
         <el-option v-for="item in fontFamilyOptions" :key="item.value" :label="item.label" :value="item.value" />
       </el-select>
@@ -99,8 +99,11 @@ export default {
       type: String,
       default: '',
     },
+    newVersion: {
+      type: Boolean,
+      default: false,
+    },
   },
-
   data() {
     return {
       isEnable,
@@ -186,6 +189,9 @@ export default {
         this.$set(this.dataContent.activeTextStyle, 'backgroundColor', '');
         this.$set(this.dataContent.activeTextStyle, 'cursor', 'text');
       }
+      if (this.newVersion) {
+        this.$set(this.dataContent, 'activeTextStyle', {});
+      }
       this.$emit('update:visible', false);
       this.$emit('fillTonePinyin', this.dataContent);
       this.numberPinyin = '';

+ 32 - 21
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -62,6 +62,7 @@
           :id="richId + '_pinyin_text'"
           ref="PinyinText"
           :paragraph-list="data.paragraph_list"
+          :rich-text-list="data.rich_text_list"
           :paragraph-version="paragraphVersion"
           :pinyin-position="data.property.pinyin_position"
           :pinyin-overall-position="data.property.pinyin_overall_position"
@@ -165,9 +166,8 @@ export default {
   },
   methods: {
     async saveWord(saveArr) {
-      console.info('saveArr', saveArr);
       const text = this.data.content.replace(/<[^>]+>/g, '');
-      await this.createParsedTextInfoPinyin(text, null, saveArr);
+      await this.createParsedTextInfoPinyin(null, null, saveArr);
       this.showWordFlag = false;
     },
     handlePinyinDisplay(property) {
@@ -185,10 +185,10 @@ export default {
         this.data.paragraph_list_parameter.text = text;
         this.data.paragraph_list_parameter.is_first_sentence_first_hz_pinyin_first_char_upper_case =
           property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
-        this.createParsedTextInfoPinyin(text);
+        this.createParsedTextInfoPinyin(null);
       }
     },
-    //生成拼音
+    // 生成拼音
     generatPinyin() {
       if (!isEnable(this.data.property.view_pinyin)) {
         return;
@@ -196,7 +196,7 @@ export default {
       let styles = this.$refs.richText.getFirstCharStyles();
       let content = this.$refs.richText.getRichContent();
       let text = content.replace(/<[^>]+>/g, '');
-      this.createParsedTextInfoPinyin(text, styles);
+      this.createParsedTextInfoPinyin(null, styles);
     },
     showSetting() {
       this.richId = this.$refs.richText.id;
@@ -207,19 +207,24 @@ export default {
     // 获取拼音解析文本
     async createParsedTextInfoPinyin(text, styles, fc_list) {
       const data = this.data.paragraph_list_parameter;
+      if (text === null) {
+        // text.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
+        text = this.data.content;
+      }
       if (text === '') {
         data.pinyin_proofread_word_list = [];
         return;
       }
-      data.text = text.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
+      data.text = text;
       data.is_first_sentence_first_hz_pinyin_first_char_upper_case =
         this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
 
       let fcList = fc_list || [];
       data.is_custom_fc = fcList.length > 0;
       data.fc_paragraph_list = fcList;
+      data.is_rich_text = 'true';
 
-      await PinyinBuild_OldFormat(data).then(({ parsed_text }) => {
+      await PinyinBuild_OldFormat(data).then(({ parsed_text, rich_text }) => {
         if (parsed_text) {
           const mergedData = parsed_text.paragraph_list.map((outerArr, i) =>
             outerArr.map((innerArr, j) =>
@@ -245,11 +250,11 @@ export default {
               }),
             ),
           );
-          //this.data.paragraph_list = mergedData; // 取出合并后的数组
           this.$set(this.data, 'paragraph_list', mergedData);
           this.paragraphVersion++;
           this.parseFClist();
         }
+        this.$set(this.data, 'rich_text_list', rich_text.text_list);
       });
     },
     // 开启拼音之后,拼音样式要跟随标题样式
@@ -294,19 +299,25 @@ export default {
       return hash.toString(36); // 转为base36缩短长度
     },
     // 填充校对后的拼音
-    fillCorrectPinyin({ selectContent: { text, pinyin, activeTextStyle, note, showPinyin }, i, j, k }) {
-      this.data.paragraph_list_parameter.pinyin_proofread_word_list.push({
-        paragraph_index: i,
-        sentence_index: j,
-        word_index: k,
-        word: text,
-        pinyin,
-        showPinyin,
-      });
-      if (pinyin) this.data.paragraph_list[i][j][k].pinyin = pinyin;
-      if (activeTextStyle) this.data.paragraph_list[i][j][k].activeTextStyle = activeTextStyle;
-      if (note) this.data.paragraph_list[i][j][k].note = note;
-      this.data.paragraph_list[i][j][k].showPinyin = showPinyin;
+    fillCorrectPinyin({ selectContent: { text, pinyin, activeTextStyle, note, showPinyin }, i, j, k, newVersion }) {
+      if (newVersion) {
+        if (pinyin) this.data.rich_text_list[j].word_list[k].pinyin = pinyin;
+        this.data.rich_text_list[j].word_list[k].showPinyin = showPinyin;
+      } else {
+        // 兼容历史数据
+        this.data.paragraph_list_parameter.pinyin_proofread_word_list.push({
+          paragraph_index: i,
+          sentence_index: j,
+          word_index: k,
+          word: text,
+          pinyin,
+          showPinyin,
+        });
+        if (pinyin) this.data.paragraph_list[i][j][k].pinyin = pinyin;
+        if (activeTextStyle) this.data.paragraph_list[i][j][k].activeTextStyle = activeTextStyle;
+        if (note) this.data.paragraph_list[i][j][k].note = note;
+        this.data.paragraph_list[i][j][k].showPinyin = showPinyin;
+      }
     },
     // 思维导图
     handlerMindMap() {

+ 1 - 0
src/views/book/courseware/data/richText.js

@@ -477,6 +477,7 @@ export function getRichTextData() {
     content: '',
     note_list: [], // 注释
     paragraph_list: [],
+    rich_text_list: [],
     paragraph_list_parameter: {
       text: '',
       is_first_sentence_first_hz_pinyin_first_char_upper_case: 'true',

+ 1 - 0
src/views/book/courseware/preview/components/rich_text/RichTextPreview.vue

@@ -8,6 +8,7 @@
         <PinyinText
           v-if="isEnable(data.property.view_pinyin)"
           :paragraph-list="data.paragraph_list"
+          :rich-text-list="data.rich_text_list"
           :pinyin-position="data.property.pinyin_position"
           :pinyin-overall-position="data.property.pinyin_overall_position"
           :pinyin-size="data?.unified_attrib?.pinyin_size"