Browse Source

1. 章节树展开收缩 2. 用户设置字号大小

dsy 3 weeks ago
parent
commit
f20060c73c

+ 144 - 9
src/components/CommonPreview.vue

@@ -76,7 +76,7 @@
               is_my_edit_task,
               is_my_audit_task,
               is_show_submit_audit_button,
-            } in node_list"
+            } in visibleNodeList"
             :key="nodeId"
             :class="['menu-item', { active: curSelectId === nodeId }, { courseware: isTrue(is_leaf_chapter) }]"
             :style="computedNameStyle(deep, isTrue(is_leaf_chapter))"
@@ -101,6 +101,14 @@
             <template v-if="!isTrue(is_leaf_chapter) && isTrue(is_show_submit_audit_button) && type === 'edit_preview'">
               <span class="link" @click="submitChapterAllCoursewareToAuditFlow(nodeId)">提交审核</span>
             </template>
+            <!-- 章节收缩/展开图标-->
+            <span v-if="!isTrue(is_leaf_chapter)" class="tree-arrow">
+              <i
+                :class="isChapterCollapsed(nodeId) ? 'el-icon-arrow-right' : 'el-icon-arrow-down'"
+                :title="isChapterCollapsed(nodeId) ? '展开' : '收缩'"
+                @click.stop="toggleChapterCollapse(nodeId)"
+              ></i>
+            </span>
           </div>
         </div>
       </aside>
@@ -407,6 +415,18 @@
                 inactive-value="zh-Hans"
                 inactive-text="简体"
               />
+              <div class="setting-item">
+                <span class="setting-item__label">字号</span>
+                <el-slider
+                  v-model="fontScalePercent"
+                  class="setting-item__slider"
+                  :min="80"
+                  :max="150"
+                  :step="5"
+                  :show-tooltip="false"
+                />
+                <span class="setting-item__value">{{ fontScalePercent }}%</span>
+              </div>
             </div>
           </div>
         </div>
@@ -522,6 +542,7 @@ import { toggleFullScreen } from '@/utils/common';
 import * as OpenCC from 'opencc-js';
 import { isTrue } from '@/utils/validate';
 import { buildCoursewareStyle } from '@/views/book/courseware/preview/common/utils/coursewareStyle';
+import { scaleComponentListFontSize } from '@/views/book/courseware/preview/common/utils/fontSizeScale';
 import { CreateCoursewarePreviewURL } from '@/api/app';
 
 export default {
@@ -629,8 +650,10 @@ export default {
         background: {},
       },
       node_list: [],
+      collapsedNodeIds: [], // 已收缩的章节节点 id
       data: { row_list: [] },
       component_list: [],
+      raw_component_list: [], // 未缩放字号的原始组件数据
       content_group_row_list: [],
       remark_list: [],
       remark_list_obj: {}, // 存放以组件为对象的批注数组
@@ -687,6 +710,7 @@ export default {
       langList: [],
       lang: 'ZH',
       chinese: 'zh-Hans', // 语言简体中文zh-Hans,繁体中文zh-Hant
+      fontScalePercent: 100, // 预览字号缩放百分比(80-150)
       showPinYin: 'false',
       curSelectId: this.id,
       navigationShow: true,
@@ -748,6 +772,27 @@ export default {
     computedCommonPreviewStyle() {
       return buildCoursewareStyle(this.background, 'commonPreview');
     },
+    /**
+     * 教材章节树实际展示的节点列表:过滤掉已收缩节点下的所有子节点
+     * @returns {Array} 需要展示的章节节点列表
+     */
+    visibleNodeList() {
+      let collapsedDeep = 0; // 当前被收缩的祖先节点层级,0 表示没有收缩
+      return (this.node_list || []).filter((node) => {
+        const deep = Number(node.deep) || 1;
+        // 处于已收缩节点之下的子节点直接过滤掉
+        if (collapsedDeep && deep > collapsedDeep) return false;
+        collapsedDeep = this.collapsedNodeIds.includes(node.id) ? deep : 0;
+        return true;
+      });
+    },
+    /**
+     * 预览字号缩放比例
+     * @returns {number} 缩放比例,如 1.2
+     */
+    fontScaleRatio() {
+      return this.fontScalePercent / 100;
+    },
   },
   watch: {
     curSelectId() {
@@ -767,6 +812,9 @@ export default {
 
       this.multimediaHandleChange();
     },
+    fontScaleRatio() {
+      this.applyFontScale();
+    },
   },
   created() {
     if (this.id) {
@@ -924,12 +972,22 @@ export default {
           });
 
           if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list) || [];
+
+          // 保留未缩放的原始组件数据,字号缩放时都从原始数据重新派生,避免多次缩放叠加
+          this.raw_component_list = this.component_list;
+          this.applyFontScale();
         })
         .catch((error) => {
           if (this.isCanceledRequest(error)) return null;
           throw error;
         });
     },
+    /**
+     * 按当前字号比例重新派生展示用的组件列表
+     */
+    applyFontScale() {
+      this.component_list = scaleComponentListFontSize(this.raw_component_list, this.fontScaleRatio);
+    },
     getLangList() {
       GetLanguageTypeList({
         book_id: this.courseware_info.book_id,
@@ -1378,6 +1436,26 @@ export default {
     },
 
     /**
+     * 判断章节节点是否已收缩
+     * @param {string} nodeId - 节点ID
+     * @returns {boolean} - 是否已收缩
+     */
+    isChapterCollapsed(nodeId) {
+      return this.collapsedNodeIds.includes(nodeId);
+    },
+    /**
+     * 切换章节节点的收缩/展开状态
+     * @param {string} nodeId - 节点ID
+     */
+    toggleChapterCollapse(nodeId) {
+      const index = this.collapsedNodeIds.indexOf(nodeId);
+      if (index === -1) {
+        this.collapsedNodeIds.push(nodeId);
+      } else {
+        this.collapsedNodeIds.splice(index, 1);
+      }
+    },
+    /**
      * 选择节点
      * @param {string} nodeId - 节点ID
      * @param {boolean} isLeaf - 是否是叶子节点
@@ -1849,8 +1927,26 @@ export default {
     computedSelectedGroupCoursewareInfo() {
       return this.$refs.courseware.computedSelectedGroupCoursewareInfo();
     },
-    saveCoursewareStyleTemplate(data) {
-      return this.$refs.courseware.saveCoursewareStyleTemplate(data);
+    /**
+     * 保存为样式模板
+     * 字号缩放仅用于预览展示,保存前临时还原为原始数据,避免把缩放后的字号写入模板
+     * @param {object} data 保存参数
+     * @returns {Promise} 保存结果
+     */
+    async saveCoursewareStyleTemplate(data) {
+      if (this.fontScaleRatio === 1 || this.raw_component_list.length === 0) {
+        return this.$refs.courseware.saveCoursewareStyleTemplate(data);
+      }
+
+      const scaledComponentList = this.component_list;
+      try {
+        this.component_list = this.raw_component_list;
+        await this.$nextTick();
+        return await this.$refs.courseware.saveCoursewareStyleTemplate(data);
+      } finally {
+        this.component_list = scaledComponentList;
+        await this.$nextTick();
+      }
     },
 
     async submitChapterAllCoursewareToAuditFlow(chapter_id) {
@@ -1877,13 +1973,14 @@ export default {
      * @param {object} background - 背景信息对象
      */
     updateComponentBackground({ courseware_id, component_id }, background) {
-      const component = this.component_list.find(
-        (c) => c.courseware_id === courseware_id && c.component_id === component_id,
-      );
+      // 缩放后的组件列表是原始列表的浅拷贝,两处都需同步,避免切换字号后组件背景丢失
+      [this.component_list, this.raw_component_list].forEach((list) => {
+        const component = list.find((c) => c.courseware_id === courseware_id && c.component_id === component_id);
 
-      if (component) {
-        this.$set(component, 'background', background);
-      }
+        if (component) {
+          this.$set(component, 'background', background);
+        }
+      });
     },
     /**
      * 获取 main-container 视口内第一个可见的组件 id
@@ -2214,6 +2311,21 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
             }
           }
 
+          .tree-arrow {
+            display: inline-flex;
+            flex: none;
+            align-items: center;
+            justify-content: center;
+            width: 16px;
+            margin-left: 4px;
+            font-size: 12px;
+            color: #909399;
+
+            i {
+              cursor: pointer;
+            }
+          }
+
           .name {
             flex: 1;
             padding: 4px 8px 4px 4px;
@@ -2443,6 +2555,29 @@ $total-width: $courseware-width + $courseware-left-margin + $courseware-right-ma
           &-content {
             padding: 10px;
           }
+
+          &-item {
+            display: flex;
+            column-gap: 8px;
+            align-items: center;
+            margin-top: 20px;
+
+            &__label {
+              font-size: 14px;
+              white-space: nowrap;
+            }
+
+            &__slider {
+              flex: 1;
+            }
+
+            &__value {
+              width: 42px;
+              font-size: 14px;
+              color: #4095e5;
+              text-align: right;
+            }
+          }
         }
       }
 

+ 128 - 0
src/views/book/courseware/preview/common/utils/fontSizeScale.js

@@ -0,0 +1,128 @@
+/**
+ * 预览字号等比缩放工具
+ *
+ * 编辑时设置的字号以 pt/px 绝对单位写入课件数据,预览时无法通过父级 font-size 继承放大,
+ * 因此在数据层按比例等比缩放,覆盖以下来源:
+ * 1. 统一属性字段:font_size / pinyin_size / fontSize(如 '14pt'、'20px'、18)
+ * 2. HTML 片段中的内联样式:style="font-size: 14pt"
+ *
+ * 说明:sizeRatio 为 1 时原样返回,保证默认(100%)行为完全不变。
+ */
+
+// 需要等比缩放的字号字段名
+const FONT_SIZE_KEYS = ['font_size', 'pinyin_size', 'fontSize'];
+
+// 可缩放的字号值:纯数字(如组件属性 font_size: 18)或 数字 + pt/px 绝对单位
+const FONT_SIZE_VALUE_RE = /^(\d+(?:\.\d+)?)(pt|px)?$/i;
+
+// HTML 内联样式中的字号:font-size: 14pt / font-size:14px
+const INLINE_FONT_SIZE_RE = /(font-size\s*:\s*)(\d+(?:\.\d+)?)(pt|px)/gi;
+
+/**
+ * 保留两位小数,避免出现 16.799999999999997 这类浮点误差
+ * @param {number} value 数值
+ * @returns {number} 保留两位小数后的数值
+ */
+function roundSize(value) {
+  return Math.round(value * 100) / 100;
+}
+
+/**
+ * 缩放单个字号值
+ * @param {string|number} value 字号值,如 '14pt'、'20px'、18
+ * @param {number} sizeRatio 缩放比例,如 1.2
+ * @returns {string|number} 缩放后的字号值;非字号值时原样返回
+ */
+export function scaleFontSizeValue(value, sizeRatio) {
+  if (sizeRatio === 1) return value;
+
+  if (typeof value === 'number') {
+    return Number.isFinite(value) ? roundSize(value * sizeRatio) : value;
+  }
+  if (typeof value !== 'string') return value;
+
+  const match = value.match(FONT_SIZE_VALUE_RE);
+  if (!match) return value;
+
+  const amount = parseFloat(match[1]);
+  if (!Number.isFinite(amount)) return value;
+
+  return `${roundSize(amount * sizeRatio)}${match[2] || ''}`;
+}
+
+/**
+ * 缩放 HTML 片段中内联样式的字号
+ * 仅处理 pt/px 绝对单位,em/百分比等相对单位会随父级一起变化,不重复缩放
+ * @param {string} html HTML 片段
+ * @param {number} sizeRatio 缩放比例
+ * @returns {string} 缩放后的 HTML 片段
+ */
+export function scaleHtmlFontSize(html, sizeRatio) {
+  if (sizeRatio === 1 || typeof html !== 'string' || !html) return html;
+  if (!/font-size/i.test(html)) return html;
+
+  return html.replace(INLINE_FONT_SIZE_RE, (match, prefix, amount, unit) => {
+    return `${prefix}${roundSize(parseFloat(amount) * sizeRatio)}${unit}`;
+  });
+}
+
+/**
+ * 递归缩放数据中的字号(不修改入参)
+ * @param {any} node 待处理数据
+ * @param {number} sizeRatio 缩放比例
+ * @returns {any} 缩放后的数据
+ */
+function scaleNodeFontSize(node, sizeRatio) {
+  if (Array.isArray(node)) {
+    return node.map((item) => scaleNodeFontSize(item, sizeRatio));
+  }
+
+  if (node && typeof node === 'object') {
+    return Object.keys(node).reduce((acc, key) => {
+      const value = node[key];
+      if (FONT_SIZE_KEYS.includes(key)) {
+        acc[key] = scaleFontSizeValue(value, sizeRatio);
+      } else if (typeof value === 'string') {
+        acc[key] = scaleHtmlFontSize(value, sizeRatio);
+      } else {
+        acc[key] = scaleNodeFontSize(value, sizeRatio);
+      }
+      return acc;
+    }, {});
+  }
+
+  return node;
+}
+
+/**
+ * 缩放组件内容 JSON 字符串中的字号
+ * @param {string} content 组件内容 JSON 字符串
+ * @param {number} sizeRatio 缩放比例
+ * @returns {string} 缩放后的组件内容 JSON 字符串
+ */
+export function scaleContentFontSize(content, sizeRatio) {
+  if (sizeRatio === 1 || typeof content !== 'string' || !content) return content;
+
+  try {
+    const data = JSON.parse(content);
+    return JSON.stringify(scaleNodeFontSize(data, sizeRatio));
+  } catch (e) {
+    return content;
+  }
+}
+
+/**
+ * 缩放组件列表中的字号
+ * 仅 content 字段参与缩放(浅拷贝),其它字段保持原引用,便于背景等修改同步
+ * @param {Array} componentList 组件列表
+ * @param {number} sizeRatio 缩放比例
+ * @returns {Array} 缩放后的组件列表
+ */
+export function scaleComponentListFontSize(componentList, sizeRatio) {
+  if (sizeRatio === 1 || !Array.isArray(componentList)) return componentList;
+
+  return componentList.map((item) => {
+    if (!item || !item.content) return item;
+    return { ...item, content: scaleContentFontSize(item.content, sizeRatio) };
+  });
+}

+ 1 - 1
src/views/login/index.vue

@@ -339,7 +339,7 @@ export default {
     },
     updateServerAddress() {
       if (process.env.NODE_ENV === 'development' && this.form.server_address !== '/production') {
-        return updateBaseURL('/production');
+        return updateBaseURL('/eep');
       }
       updateBaseURL(this.form.server_address);
     },

+ 1 - 1
src/views/personal_workbench/project/ProductionEditorialManage.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="production-editorial">
     <div class="production-editorial__header">
-      <span class="name">{{ project_info.name }}</span>
+      <span class="name nowrap-ellipsis" :title="project_info.name">{{ project_info.name }}</span>
       <div class="courseware">
         <div class="audit-step">
           <span>审核步骤:</span>