浏览代码

富文本拼音效果下 数字和字母作为拼音的居中显示

zq 6 天之前
父节点
当前提交
dee0b83dd2

+ 69 - 2
src/components/PinyinText.vue

@@ -43,7 +43,7 @@
                   'align-items': getWordAlignItems(word, block),
                 }"
               >
-                <span class="pinyin" :style="getPinyinStyle(word, block)">
+                <span class="pinyin" :style="[getPinyinStyle(word, block), getWholePinyinStyle(word, block)]">
                   {{ getPinyinText(word) }}
                 </span>
                 <span class="py-char" :style="getCharStyle(word, block)">{{ convertText(word.text) }}</span>
@@ -829,7 +829,12 @@ export default {
     },
     // 兼容历史数据
     getPinyinText(item) {
-      return this.checkShowPinyin(item.showPinyin) ? item.pinyin : '\u200B';
+      if (!this.checkShowPinyin(item.showPinyin)) return '\u200B';
+      const raw = item.pinyin || '';
+      // 整词渲染时去掉首尾占位空格(这些空格只在逐字对齐时有意义),
+      // 避免占位空格撑宽拼音导致居中偏移
+      const text = String(raw).replace(/^[\u3000\s]+|[\u3000\s]+$/g, '');
+      return text || '\u200B';
     },
     shouldShowWordPinyin(item) {
       return this.checkShowPinyin(item && item.showPinyin);
@@ -855,6 +860,68 @@ export default {
       }
       return 'center';
     },
+    // 判断字符是否半宽(ASCII / 拉丁字母及声调字符等),用于估算文字视觉宽度
+    isHalfWidthChar(char) {
+      const code = char.charCodeAt(0);
+      // 基础拉丁、拉丁扩展 A/B(拼音带声调字符大多在此区间)
+      if (code <= 0x024f) return true;
+      return false;
+    },
+    // 字符视觉宽度单位:中文/全角=1,英文/数字/拉丁字母≈0.5,组合音标不占宽
+    getCharWidthUnit(char) {
+      const code = char.charCodeAt(0);
+      if (code >= 0x0300 && code <= 0x036f) return 0;
+      return this.isHalfWidthChar(char) ? 0.5 : 1;
+    },
+    // 整词渲染时,让拼音只横跨非标点文字区域并居中;
+    getWholePinyinStyle(word, block) {
+      const letterSpacing = block?.styleObj?.letterSpacing;
+      if (letterSpacing && letterSpacing !== '0' && letterSpacing !== '0px') {
+        return {};
+      }
+      const text = word.text || '';
+      const chars = Array.from(text);
+      if (chars.length === 0) {
+        return { alignSelf: 'stretch', textAlign: 'center' };
+      }
+
+      let totalUnits = 0;
+      let leadingUnits = 0;
+      let trailingUnits = 0;
+
+      chars.forEach((c) => {
+        totalUnits += this.getCharWidthUnit(c);
+      });
+
+      for (let i = 0; i < chars.length; i += 1) {
+        PUNCT_REGEX.lastIndex = 0;
+        if (PUNCT_REGEX.test(chars[i])) {
+          leadingUnits += this.getCharWidthUnit(chars[i]);
+        } else {
+          break;
+        }
+      }
+      for (let i = chars.length - 1; i >= 0; i -= 1) {
+        PUNCT_REGEX.lastIndex = 0;
+        if (PUNCT_REGEX.test(chars[i])) {
+          trailingUnits += this.getCharWidthUnit(chars[i]);
+        } else {
+          break;
+        }
+      }
+
+      const nonPunctUnits = totalUnits - leadingUnits - trailingUnits;
+      if (nonPunctUnits <= 0 || totalUnits <= 0 || (leadingUnits === 0 && trailingUnits === 0)) {
+        return { alignSelf: 'stretch', textAlign: 'center' };
+      }
+
+      return {
+        alignSelf: 'flex-start',
+        width: `${(nonPunctUnits / totalUnits) * 100}%`,
+        marginLeft: `${(leadingUnits / totalUnits) * 100}%`,
+        textAlign: 'center',
+      };
+    },
     // 拼音固定为拼音字体,跟随汉字的字号、颜色、粗细的样式
     getPinyinStyle(item, block) {
       const styles = {};

+ 20 - 3
src/views/book/courseware/create/components/base/common/CorrectPinyin.vue

@@ -98,7 +98,7 @@ import { addTone, handleToneValue } from '@/utils/common';
 import RichText from '@/components/RichText.vue';
 import _ from 'lodash';
 import { fontFamilyList } from '@/views/book/courseware/data/table.js';
-import { isEnable, PUNCT_REGEX } from '@/views/book/courseware/data/common';
+import { isEnable, PUNCT_REGEX, LEADING_PUNCT, TRAILING_PUNCT } from '@/views/book/courseware/data/common';
 import { toolGetWordPinyinCorrectionList } from '@/api/pinyinCorrection';
 
 export default {
@@ -190,8 +190,25 @@ export default {
     convertTonePinyin() {
       if (!this.numberPinyin) return;
       let newPinyin = this.handleReplaceTone(this.numberPinyin.replace(/\s+| +/g, ''));
-      const leading = (this.dataContent.pinyin.match(/^[ ]+/) || [''])[0];
-      const trailing = (this.dataContent.pinyin.match(/[ ]+$/) || [''])[0];
+      // 原始文本(含标点),用于判断标点在词首还是词尾,决定保留哪一侧的占位空格
+      const rawText = this.selectContent?.text || '';
+      const chars = Array.from(rawText);
+      const hasLeadingPunct = chars.length > 0 && LEADING_PUNCT.has(chars[0]);
+      const hasTrailingPunct = chars.length > 0 && TRAILING_PUNCT.has(chars[chars.length - 1]);
+
+      const pinyinStr = this.dataContent.pinyin || '';
+      let leading = '';
+      let trailing = '';
+      // 先取词首标点占位(前缀全角空格),再从剩余部分取词尾标点占位(后缀全角空格),
+      // 避免同一个空格既被当作 leading 又被当作 trailing 而被复制成两个
+      let rest = pinyinStr;
+      if (hasLeadingPunct) {
+        leading = (rest.match(/^[ ]+/) || [''])[0];
+        rest = rest.slice(leading.length);
+      }
+      if (hasTrailingPunct) {
+        trailing = (rest.match(/[ ]+$/) || [''])[0];
+      }
       this.dataContent.pinyin = leading + newPinyin + trailing;
     },
     handleReplaceTone(e) {