3 Commits 72c205f92f ... dee0b83dd2

Auteur SHA1 Bericht Datum
  zq dee0b83dd2 富文本拼音效果下 数字和字母作为拼音的居中显示 6 dagen geleden
  dsy 51e64c7cfd 1. 语音矩阵录音跟读功能 2.纯文本切换提示 3. 排序题占位符修改与提示 1 week geleden
  dsy 3678715936 分组线单独放一个容器,避免dom影响其它组件 1 week geleden

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2026.08.21'
+VUE_APP_VERSION = '2026.08.22'

+ 1 - 1
packages/package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep-ui",
-  "version": "0.0.41",
+  "version": "0.0.42",
   "main": "eep/eep-ui.umd.js",
   "style": "eep/eep-ui.css",
   "exports": {

+ 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 = {};

+ 18 - 7
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -1,13 +1,17 @@
 <template>
   <main ref="canvas" class="canvas">
     <div v-if="isEdit" class="edit">
-      <div
-        v-for="item in lineList"
-        :key="item[0]"
-        class="group-line"
-        :data-canvas-height="canvasHeight"
-        :style="computedGroupLine(item)"
-      ></div>
+      <!-- 分组线单独放在一个绝对定位容器中,避免与行列表在同一 children 列表里 diff,
+        否则删除被分组的行时 Vue 会移动上方行的 DOM,导致 tinymce iframe 重载、内容丢失 -->
+      <div class="group-lines">
+        <div
+          v-for="item in lineList"
+          :key="item[0]"
+          class="group-line"
+          :data-canvas-height="canvasHeight"
+          :style="computedGroupLine(item)"
+        ></div>
+      </div>
       <span class="drag-line" data-row="-1"></span>
       <!-- 行 -->
       <template v-for="(row, i) in data.row_list">
@@ -2009,6 +2013,13 @@ export default {
     margin: 0 auto;
     background-color: #fff;
 
+    .group-lines {
+      position: absolute;
+      top: 0;
+      left: 0;
+      pointer-events: none;
+    }
+
     .group-line {
       position: absolute;
       left: 11px;

+ 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) {

+ 2 - 1
src/views/book/courseware/create/components/question/sort/Sort.vue

@@ -5,9 +5,10 @@
         <el-input
           v-model="data.content"
           type="textarea"
-          placeholder="你 喜欢 喝 茶 还是 咖啡 ?"
+          placeholder="茶 喜欢 咖啡 还是 你 喝 ?"
           :autosize="{ minRows: 2, maxRows: 6 }"
           resize="none"
+          title="请根据显示顺序输入"
           @change="handleContentChange"
         />
 

+ 1 - 0
src/views/book/courseware/create/components/question/voice_matrix/VoiceMatrix.vue

@@ -49,6 +49,7 @@
               <span
                 class="toggle-pill"
                 :class="{ 'is-active': li.is_only_text }"
+                title="按钮为纯文本模式,开启后仅显示内容,不执行音频打点、播放"
                 @click="toggleOnlyText(li, !li.is_only_text)"
               >
                 <span class="toggle-dot"></span>

+ 2 - 1
src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue

@@ -38,7 +38,7 @@
       <!-- 语音矩阵 -->
       <div
         class="voice-matrix-container"
-        :style="{ height: isEnable(data.property.is_enable_record) ? 'calc(100% - 135px)' : 'auto' }"
+        :style="{ height: isEnable(data.property.is_enable_record) ? 'calc(100% - 115px)' : 'auto' }"
       >
         <div
           v-if="data.option_list.length > 0"
@@ -185,6 +185,7 @@
           type="promax"
           class="luyin-box"
           :answer-record-list="data.record_list"
+          :select-data="selectData"
           :attrib="data.unified_attrib"
           @getWavblob="getWavblob"
           @getSelectData="getSelectData"