dsy před 2 týdny
rodič
revize
7ff3ca7bd2

+ 1 - 1
.env

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

+ 1 - 1
packages/package.json

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

+ 4 - 1
src/components/CommonPreview.vue

@@ -7,7 +7,9 @@
       </div>
 
       <div class="courseware-top">
-        <span class="name-path">{{ courseware_info.name_path }}</span>
+        <span class="name-path nowrap-ellipsis" :title="courseware_info.name_path">
+          {{ courseware_info.name_path }}
+        </span>
         <span class="flow-nodename">{{ courseware_info.cur_audit_flow_node_name }}</span>
         <slot name="middle" :courseware="courseware_info"></slot>
         <template v-if="type === 'edit_preview'">
@@ -2051,6 +2053,7 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
       display: flex;
       justify-content: space-between;
       width: 360px;
+      min-width: 320px;
       padding: 4px 8px;
       border-right: $border;
     }

+ 9 - 1
src/components/PinyinText.vue

@@ -26,7 +26,7 @@
             v-for="(word, wIndex) in block.word_list"
             :key="wIndex"
             :class="['pinyin-text', { spacing: isShowPinyin }]"
-            :style="block.styleObj"
+            :style="[block.styleObj, getLineHeightSpacingStyle(block)]"
           >
             <span
               :title="isPreview ? '' : '点击校对'"
@@ -744,6 +744,14 @@ export default {
       };
     },
 
+    getLineHeightSpacingStyle(block) {
+      const lineHeight = block?.sentenceStyle?.lineHeight;
+      if (lineHeight === undefined || !/^\d*\.?\d+$/.test(String(lineHeight).trim())) return {};
+
+      const spacing = Number(lineHeight) - 1.5;
+      return spacing ? { marginBottom: `${spacing}em` } : {};
+    },
+
     // 解析样式字符串为对象
     parseStyleToObject(styleStr) {
       const styleObj = {};

+ 56 - 29
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -374,18 +374,18 @@ export default {
     });
     ContentGetCoursewareContent({ id: this.courseware_id }).then(
       ({ content, content_group_row_list, title_list, my_project_resource_popedom }) => {
+        const groupRowList = content_group_row_list ? JSON.parse(content_group_row_list) : [];
         if (content) {
           let parsedContent = JSON.parse(content);
-          if (
-            parsedContent &&
-            Array.isArray(parsedContent.row_list) &&
-            parsedContent.row_list.length > 0 &&
-            !parsedContent.row_list[0]?.row_id
-          ) {
-            this.data = this.normalizeRowColIds(parsedContent);
-          } else {
-            this.data = parsedContent;
+          if (parsedContent && Array.isArray(parsedContent.row_list)) {
+            parsedContent = this.normalizeRowColIds(parsedContent);
+            const normalized = this.normalizeCanvasRows(parsedContent.row_list, groupRowList);
+            parsedContent.row_list = normalized.rowList;
+            this.content_group_row_list = normalized.groupRowList;
           }
+          this.data = parsedContent;
+        } else {
+          this.content_group_row_list = [];
         }
         const timer = setInterval(() => {
           if (this.componentIsAllLoader()) {
@@ -394,8 +394,6 @@ export default {
             this.componentsDataLoaded = true;
           }
         }, 200);
-        if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
-
         if (title_list) this.title_list = title_list || [];
 
         this.$watch(
@@ -1725,6 +1723,46 @@ export default {
       return data;
     },
     /**
+     * 过滤无组件的空行,并将分组记录按行顺序归一化为一行一条。
+     * @param {Array} rowList 行列表
+     * @param {Array} groupList 行分组列表
+     * @param {boolean} regenerateRowIds 是否为所有行重新生成 ID
+     */
+    normalizeCanvasRows(rowList, groupList, regenerateRowIds = false) {
+      const groupQueues = new Map();
+      (groupList || []).forEach((group) => {
+        if (!groupQueues.has(group.row_id)) groupQueues.set(group.row_id, []);
+        groupQueues.get(group.row_id).push(group);
+      });
+
+      const normalizedRows = [];
+      const normalizedGroups = [];
+      const usedRowIds = new Set();
+
+      (rowList || []).forEach((row) => {
+        const sourceRowId = row?.row_id;
+        const group = groupQueues.get(sourceRowId)?.shift() || {};
+        const columns = Array.isArray(row?.col_list) ? row.col_list : [];
+        if (!columns.some((col) => Array.isArray(col?.grid_list) && col.grid_list.length > 0)) return;
+
+        let rowId = sourceRowId;
+        if (regenerateRowIds || !rowId || usedRowIds.has(rowId)) {
+          do {
+            rowId = `R${getRandomNumber(6, true)}`;
+          } while (usedRowIds.has(rowId));
+        }
+        usedRowIds.add(rowId);
+        normalizedRows.push({ ...row, row_id: rowId });
+        normalizedGroups.push({
+          ...group,
+          row_id: rowId,
+          is_pre_same_group: normalizedGroups.length > 0 && Boolean(group.is_pre_same_group),
+        });
+      });
+
+      return { rowList: normalizedRows, groupRowList: normalizedGroups };
+    },
+    /**
      * 删除行后归一化行分组列表
      *
      * is_pre_same_group 为 true 表示该行与其前一行同属一个分组。
@@ -1775,9 +1813,10 @@ export default {
       // 深拷贝:避免与「选择模板」弹窗缓存的模板数据共享引用(否则画布改动会污染模板数据)
       const templateData = _.cloneDeep(data) || { row_list: [] };
       if (!Array.isArray(templateData.row_list)) templateData.row_list = [];
+      const normalized = this.normalizeCanvasRows(templateData.row_list, _.cloneDeep(content_group_row_list) || []);
+      templateData.row_list = normalized.rowList;
       this.data = templateData;
-      // 分组行列表同样深拷贝:归一化时会直接改写其中的 is_pre_same_group
-      this.content_group_row_list = _.cloneDeep(content_group_row_list) || [];
+      this.content_group_row_list = normalized.groupRowList;
       // 清空模板数据中残留的 oldId,避免按陈旧的来源组件 id 取到错误内容
       this.clearGridOldId(this.data.row_list);
       await this.insertComponentBackground(this.data.row_list, backgroundList, 'id');
@@ -1791,25 +1830,13 @@ export default {
      * @param {Array} param.backgroundList - 组件背景列表
      */
     async insertTemplateData_CreateCanvas({ row_list = [], content_group_row_list = [], backgroundList = [] }) {
-      let contentGroupRowList = JSON.parse(JSON.stringify(content_group_row_list));
+      const normalized = this.normalizeCanvasRows(_.cloneDeep(row_list), content_group_row_list, true);
+      let contentGroupRowList = normalized.groupRowList;
       // 插入的内容保持独立分组,不并入插入位置上方的分组(模板数据异常时兜底)
       if (contentGroupRowList.length > 0) contentGroupRowList[0].is_pre_same_group = false;
 
-      // 深拷贝后再改写 id:直接改写会污染「选择模板」弹窗缓存的模板数据,
-      // 导致同一模板连续插入两次时把同一批对象重复放进 row_list(重复 key / 重复组件 id / oldId 被覆盖)
-      let rowList = _.cloneDeep(row_list).map((row) => {
-        const oldRowID = row.row_id;
-        const newRowID = `R${getRandomNumber(6, true)}`;
-        row.row_id = newRowID;
-        // 更新内容分组行列表中的 row_id
-        if (oldRowID) {
-          for (let idx = 0; idx < contentGroupRowList.length; idx++) {
-            if (contentGroupRowList[idx].row_id === oldRowID) {
-              contentGroupRowList[idx] = { ...contentGroupRowList[idx], row_id: newRowID };
-            }
-          }
-        }
-
+      // 深拷贝后再改写列和组件 id,避免污染「选择模板」弹窗缓存的数据。
+      let rowList = normalized.rowList.map((row) => {
         row.col_list = (row.col_list || []).map((col) => {
           col.col_id = `C${getRandomNumber(8, true)}`;
 

+ 15 - 1
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -10,7 +10,13 @@
   >
     <!-- @mouseleave="endSelection" -->
     <template v-for="(row, i) in data.row_list">
-      <div v-show="computedRowVisibility(row.row_id)" :key="i" class="row" :style="getMultipleColStyle(i)">
+      <div
+        v-if="hasRenderableComponents(row)"
+        v-show="computedRowVisibility(row.row_id)"
+        :key="i"
+        class="row"
+        :style="getMultipleColStyle(i)"
+      >
         <el-checkbox
           v-if="
             isShowGroup &&
@@ -329,6 +335,14 @@ export default {
   },
   methods: {
     /**
+     * 判断行内是否至少包含一个可渲染组件
+     * @param {Object} row 行数据
+     * @returns {boolean} 是否包含组件
+     */
+    hasRenderableComponents(row) {
+      return Boolean(row?.col_list?.some((col) => col?.grid_list?.length));
+    },
+    /**
      * 滚动到 visible_id 的组件
      */
     async handleScrollVisibleComponent() {

+ 16 - 0
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -40,6 +40,7 @@
                           '--fill-font-family': data.property.fill_font,
                           width: (inputWidthMap[li.mark] || data.property.input_default_width) + 'px',
                           marginBottom: resolveFillControlMargin(li),
+                          ...getFillControlAlignmentStyle(li),
                         },
                       ]"
                       @input="handleInput(li.input, li.mark)"
@@ -337,6 +338,21 @@ export default {
       if (fsUnit === 'pt') return `${amount * fsAmount}pt`;
       return `${amount * fsAmount}px`;
     },
+    getFillControlAlignmentStyle(li) {
+      const tagItem = (li?.rich_text_list || []).find((item) =>
+        /^<(p|div|h[1-6]|li|blockquote)\b[^>]*>/i.test(String(item?.text || '')),
+      );
+      const lineHeight = String(tagItem?.text || '').match(/line-height\s*:\s*([^;"']+)/i);
+      if (!lineHeight || !/^\d*\.?\d+$/.test(lineHeight[1].trim())) return {};
+
+      const ratio = Number(lineHeight[1].trim());
+      if (ratio < 0 || ratio >= 1.5) return {};
+      return {
+        marginBottom: '',
+        position: 'relative',
+        top: `${1.5 - ratio}em`,
+      };
+    },
     handleWav(data) {
       this.data.record_list = data;
     },