2 Commits 325b8fd442 ... 0b8d446995

Author SHA1 Message Date
  natasha 0b8d446995 Merge branch 'lhd' 3 weeks ago
  dsy aefc797cea 1、主题色、文字颜色、背景色增加历史颜色 2、编辑与预览切换时定位视口最上方组件 3 weeks ago

+ 1 - 1
.env

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

+ 21 - 0
src/api/project.js

@@ -373,3 +373,24 @@ export function GetProjectInvitePersonList(data) {
 export function DeleteProjectInvitePerson(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_manager-DeleteProjectInvitePerson`, data);
 }
+
+/**
+ * @description 得到背景色列表(常用)
+ * @param {object} data
+ */
+export function GetBackgroundColorList_Common(data) {
+  return http.post(
+    `${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetBackgroundColorList_Common`,
+    data,
+  );
+}
+/**
+ * @description 得到背景色列表(最近使用)
+ * @param {object} data
+ */
+export function GetBackgroundColorList_RecentlyUsed(data) {
+  return http.post(
+    `${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-GetBackgroundColorList_RecentlyUsed`,
+    data,
+  );
+}

+ 83 - 0
src/components/ColorPicker.vue

@@ -0,0 +1,83 @@
+<template>
+  <el-color-picker
+    :predefine="groupedPredefineColors"
+    popper-class="color-picker has-history"
+    v-bind="$attrs"
+    v-on="$listeners"
+  />
+</template>
+
+<script>
+import { GetBackgroundColorList_Common, GetBackgroundColorList_RecentlyUsed } from '@/api/project';
+
+export default {
+  name: 'ColorPicker',
+  inheritAttrs: false,
+  data() {
+    return {
+      commonList: [], // 常用颜色
+      recentlyUsedList: [], // 制作人员历史颜色
+    };
+  },
+  computed: {
+    groupedPredefineColors() {
+      return [
+        ...this.commonList.map(({ color }) => `#${color}`),
+        ...this.recentlyUsedList.map(({ color }) => `#${color}`),
+      ];
+    },
+  },
+  created() {
+    this.getBackgroundColorList_Common();
+    this.getBackgroundColorList_RecentlyUsed();
+  },
+  methods: {
+    getBackgroundColorList_Common() {
+      GetBackgroundColorList_Common().then(({ color_list }) => {
+        this.commonList = color_list;
+      });
+    },
+    getBackgroundColorList_RecentlyUsed() {
+      GetBackgroundColorList_RecentlyUsed().then(({ color_list }) => {
+        this.recentlyUsedList = color_list;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+.color-picker {
+  .el-color-predefine__colors {
+    display: flex;
+    flex-wrap: wrap;
+
+    &::before {
+      flex: 0 0 100%;
+      order: 0;
+      margin-bottom: 8px;
+      font-size: 12px;
+      color: #666;
+      content: '常用颜色';
+    }
+  }
+
+  .el-color-predefine__color-selector:nth-child(-n + 20) {
+    order: 1;
+  }
+
+  &.has-history .el-color-predefine__colors::after {
+    flex: 0 0 100%;
+    order: 2;
+    margin-top: 10px;
+    margin-bottom: 8px;
+    font-size: 12px;
+    color: #666;
+    content: '制作人员历史颜色';
+  }
+
+  .el-color-predefine__color-selector:nth-child(n + 21) {
+    order: 3;
+  }
+}
+</style>

+ 30 - 3
src/components/CommonPreview.vue

@@ -1192,7 +1192,7 @@ export default {
           this.total_count = total_count;
 
           this.file_list = resource_list || [];
-          this.file_list.forEach((x, i) => {
+          this.file_list.forEach((x) => {
             this.$set(x, 'is_can_edit', x.is_can_edit === 'true');
             this.$set(x, 'is_hided', x.is_hided === 'true');
           });
@@ -1224,7 +1224,7 @@ export default {
      */
     handleMediaShowChange(item) {
       const params = { id: item.id, is_hided: item.is_hided ? 'true' : 'false' };
-      SetBookResourceHide(params).then((res) => {
+      SetBookResourceHide(params).then(() => {
         // 如果不是显示全部资源,则需要重新加载
         if (!this.multimediaIsAllShow) {
           this.multimediaHandleChange();
@@ -1713,7 +1713,7 @@ export default {
         .then(() => {
           this.allFeedbackList = this.allFeedbackList.filter((x) => x.id !== feedBackId);
         })
-        .catch((err) => {});
+        .catch(() => {});
     },
 
     getSearch(params) {
@@ -1788,6 +1788,33 @@ export default {
     computedCommonPreviewStyle() {
       return this.$refs.courserware?.computedCourserwareStyle('commonPreview');
     },
+    /**
+     * 获取 main-container 视口内第一个可见的组件 id
+     * @returns {string} 第一个可见组件 id;不存在时返回空字符串
+     */
+    getFirstVisibleComponentId() {
+      const container = this.$refs.previewMain;
+      if (!container) return '';
+
+      const containerRect = container.getBoundingClientRect();
+      const gridElements = container.querySelectorAll('.grid[data-id]');
+
+      let firstVisibleId = '';
+      let minTop = Number.POSITIVE_INFINITY; // 初始化为正无穷大,以确保任何可见元素的 top 都会小于它
+
+      gridElements.forEach((el) => {
+        const rect = el.getBoundingClientRect();
+        const visibleHeight = Math.min(rect.bottom, containerRect.bottom) - Math.max(rect.top, containerRect.top);
+
+        const isVisible = visibleHeight > 0;
+        if (isVisible && rect.top < minTop) {
+          minTop = rect.top;
+          firstVisibleId = el.dataset.id || '';
+        }
+      });
+
+      return firstVisibleId;
+    },
   },
 };
 </script>

+ 41 - 0
src/styles/element.scss

@@ -36,3 +36,44 @@ label.el-radio {
     border-color: $main-color;
   }
 }
+
+/**
+  * 颜色选择器预设颜色样式
+  * @param {string} $class - 组件的 popper-class 类名
+  * @param {number} $commonColorNum - 常用颜色数量,默认为 20
+  */
+@mixin el-color-picker($class, $commonColorNum: 20) {
+  .#{$class} {
+    .el-color-predefine__colors {
+      display: flex;
+      flex-wrap: wrap;
+
+      &::before {
+        flex: 0 0 100%;
+        order: 0;
+        margin-bottom: 8px;
+        font-size: 12px;
+        color: #666;
+        content: '常用颜色';
+      }
+    }
+
+    .el-color-predefine__color-selector:nth-child(-n + #{$commonColorNum}) {
+      order: 1;
+    }
+
+    &.has-history .el-color-predefine__colors::after {
+      flex: 0 0 100%;
+      order: 2;
+      margin-top: 10px;
+      margin-bottom: 8px;
+      font-size: 12px;
+      color: #666;
+      content: '制作人员历史颜色';
+    }
+
+    .el-color-predefine__color-selector:nth-child(n + #{ $commonColorNum + 1 }) {
+      order: 3;
+    }
+  }
+}

+ 20 - 0
src/utils/common.js

@@ -178,3 +178,23 @@ export function getPlainText(html) {
   const doc = parser.parseFromString(html, 'text/html');
   return doc.body.textContent || '';
 }
+
+/**
+ * 等待页面布局稳定后执行回调
+ * @param {number} frames 等待的帧数
+ * @returns {Promise<void>}
+ */
+export function waitLayoutStable(frames = 2) {
+  return new Promise((resolve) => {
+    let count = 0;
+    const tick = () => {
+      count += 1;
+      if (count >= frames) {
+        resolve();
+        return;
+      }
+      requestAnimationFrame(tick);
+    };
+    requestAnimationFrame(tick);
+  });
+}

+ 74 - 2
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -59,7 +59,8 @@
                   ref="component"
                   :key="`grid-${grid.id}`"
                   :old-id="grid?.oldId"
-                  :class="[grid.id]"
+                  :class="['grid', grid.id]"
+                  :data-id="grid.id"
                   :data-row="i"
                   :data-col="j"
                   :data-grid="k"
@@ -128,6 +129,7 @@ import {
 } from '@/api/book';
 import _ from 'lodash';
 import { unified_attrib } from '@/common/data';
+import { waitLayoutStable } from '@/utils/common';
 
 import PreviewEdit from './PreviewEdit.vue';
 
@@ -187,6 +189,8 @@ export default {
       title_list: [],
       curComponentId: '', // 当前选中组件 id
       projectResourcePopedom: [], // 当前编辑人员的项目资源权限
+      componentsDataLoaded: false, // 组件数据是否加载完成
+      visible_id: this.$route.query.visible_id, // 可见组件 id
     };
   },
   computed: {
@@ -313,6 +317,30 @@ export default {
       },
       deep: true,
     },
+    componentsDataLoaded: {
+      async handler(val) {
+        if (val && this.visible_id) {
+          await this.$nextTick();
+          let isAllLoader = false;
+          if (this.$refs?.component === undefined || this.$refs?.component.length === 0) {
+            isAllLoader = true;
+          } else {
+            isAllLoader = this.$refs?.component?.every((item) => item.property.isGetContent);
+          }
+          if (isAllLoader) {
+            this.visibleIdScrollIntoView();
+          } else {
+            let checkInterval = setInterval(() => {
+              let isAllLoader = this.$refs?.component?.every((item) => item.property.isGetContent);
+              if (isAllLoader) {
+                clearInterval(checkInterval);
+                this.visibleIdScrollIntoView();
+              }
+            }, 100);
+          }
+        }
+      },
+    },
   },
   created() {
     const loading = this.$loading({
@@ -334,6 +362,7 @@ export default {
           } else {
             this.data = parsedContent;
           }
+          this.componentsDataLoaded = true;
           loading.close();
         }
         if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list);
@@ -369,6 +398,21 @@ export default {
   },
   methods: {
     /**
+     * 将可见组件滚动到视图中心
+     */
+    async visibleIdScrollIntoView() {
+      await this.$nextTick();
+      const componentNum = this.$refs?.component?.length || 0;
+      const frames = Math.ceil(componentNum / 10); // 每10个组件为一帧,计算需要等待的帧数
+      await waitLayoutStable(frames);
+
+      const target = this.findChildComponentByKey(`grid-${this.visible_id}`);
+      const targetElement = target?.$el || target;
+      if (targetElement && typeof targetElement.scrollIntoView === 'function') {
+        targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
+      }
+    },
+    /**
      * 监听复制事件,触发复制组件方法
      * @param {KeyboardEvent} event 键盘事件
      */
@@ -630,7 +674,7 @@ export default {
 
         this.$message.success('保存成功');
         if (type === 'quit') {
-          this.$emit('back');
+          this.$emit('back', this.getFirstVisibleComponentId());
         }
         if (type === 'edit') {
           this.$emit('changeEditStatus');
@@ -640,6 +684,34 @@ export default {
       }
     },
     /**
+     * 获取 create-middle 视口内第一个可见的组件 id
+     * @returns {string} 第一个可见组件 id;不存在时返回空字符串
+     */
+    getFirstVisibleComponentId() {
+      const container = document.querySelector('.create-middle');
+      if (!container) return '';
+
+      const containerRect = container.getBoundingClientRect();
+      const gridElements = container.querySelectorAll('.canvas .grid[data-id]');
+
+      let firstVisibleId = '';
+      let minTop = Number.POSITIVE_INFINITY; // 初始化为正无穷大,以确保任何可见元素的 top 都会小于它
+
+      gridElements.forEach((el) => {
+        const rect = el.getBoundingClientRect();
+        const visibleHeight = Math.min(rect.bottom, containerRect.bottom) - Math.max(rect.top, containerRect.top);
+
+        const isVisible = visibleHeight > 0;
+
+        if (isVisible && rect.top < minTop) {
+          minTop = rect.top;
+          firstVisibleId = el.dataset.id || '';
+        }
+      });
+
+      return firstVisibleId;
+    },
+    /**
      * 显示设置
      * @param {object} setting 组件设置数据
      * @param {string} type 组件类型

+ 7 - 2
src/views/book/courseware/create/components/FullTextSettings.vue

@@ -10,7 +10,7 @@
     <el-form ref="form" :model="unified_attrib" label-width="80px" size="small">
       <el-form-item label="主题色">
         <div class="color-group">
-          <el-color-picker v-model="unified_attrib.topic_color" />
+          <ColorPicker v-model="unified_attrib.topic_color" />
           <span>辅助色</span>
           <el-color-picker v-model="unified_attrib.assist_color" />
           <span class="link" @click="generateAssistColor">自动生成辅助色</span>
@@ -79,7 +79,7 @@
         </el-button>
       </el-form-item>
       <el-form-item label="文字颜色">
-        <el-color-picker v-model="unified_attrib.text_color" />
+        <ColorPicker v-model="unified_attrib.text_color" />
         <el-button type="primary" class="row-button" @click="applySingleAttrToSelectedComponents('text_color')">
           应用选中组件
         </el-button>
@@ -143,6 +143,8 @@
 </template>
 
 <script>
+import ColorPicker from '@/components/ColorPicker.vue';
+
 import { pinyinPositionList, fontList, fontSizeList, isEnable } from '@/views/book/courseware/data/common';
 import { GetBookUnifiedAttrib } from '@/api/book';
 import { unified_attrib } from '@/common/data';
@@ -150,6 +152,9 @@ import { ToAuxiliaryColor } from '@/api/app';
 
 export default {
   name: 'FullTextSettings',
+  components: {
+    ColorPicker,
+  },
   props: {
     visible: {
       type: Boolean,

+ 6 - 1
src/views/book/courseware/create/components/SetBackground.vue

@@ -96,7 +96,7 @@
             <el-checkbox v-model="background.has_color">颜色</el-checkbox>
           </div>
           <div class="setup-content">
-            <el-color-picker v-model="background.color" show-alpha />
+            <ColorPicker v-model="background.color" show-alpha />
           </div>
         </div>
         <div class="setup-item">
@@ -156,10 +156,15 @@
 </template>
 
 <script>
+import ColorPicker from '@/components/ColorPicker.vue';
+
 import { fileUpload } from '@/api/app';
 
 export default {
   name: 'SetBackground',
+  components: {
+    ColorPicker,
+  },
   props: {
     visible: {
       type: Boolean,

+ 6 - 1
src/views/book/courseware/create/components/SetComponentBackground.vue

@@ -87,7 +87,7 @@
             <el-radio v-model="background.mode" label="color">颜色</el-radio>
           </div>
           <div class="setup-content">
-            <el-color-picker v-model="background.color" show-alpha />
+            <ColorPicker v-model="background.color" show-alpha />
           </div>
         </div>
         <div class="setup-item">
@@ -142,10 +142,15 @@
 </template>
 
 <script>
+import ColorPicker from '@/components/ColorPicker.vue';
+
 import { fileUpload } from '@/api/app';
 
 export default {
   name: 'SetComponentBackground',
+  components: {
+    ColorPicker,
+  },
   props: {
     visible: {
       type: Boolean,

+ 22 - 10
src/views/book/courseware/create/components/base/rich_text/RichText.vue

@@ -181,7 +181,7 @@ export default {
       deep: true,
     },
     'data.property.is_title': {
-      handler(newLevel) {
+      handler() {
         this.$nextTick(() => {
           if (!isEnable(this.data.property.is_title)) {
             this.data.property.title_style_level = '';
@@ -257,6 +257,12 @@ export default {
       await this.createParsedTextInfoPinyin(null, null, saveArr);
       this.showWordFlag = false;
     },
+    /**
+     * 处理拼音显示逻辑:根据是否启用拼音和相关参数,决定是否生成拼音信息,并更新段落列表
+     * @param {object} property - 当前设置属性对象,包含拼音显示相关设置
+     * @property {'true' | 'false'} property.view_pinyin - 是否启用拼音显示
+     * @property {'true' | 'false'} property.is_first_sentence_first_hz_pinyin_first_char_upper_case - 是否首句首字母大写
+     */
     handlePinyinDisplay(property) {
       const text = this.data.content.replace(/<[^>]+>/g, '');
       if (!isEnable(property.view_pinyin)) {
@@ -290,17 +296,23 @@ export default {
         richId: this.richId,
       });
     },
-    // 获取拼音解析文本
+    /**
+     * 获取拼音解析文本
+     * @param {string} text - 需要解析的文本内容
+     * @param {object} styles - 需要应用的样式对象
+     * @param {array} fc_list - 分词校对后的结果列表
+     */
     async createParsedTextInfoPinyin(text, styles, fc_list) {
       const data = this.data.paragraph_list_parameter;
-      if (text === null) {
-        text = this.data.content;
+      let _text = text;
+      if (_text === null) {
+        _text = this.data.content;
       }
-      if (text === '') {
+      if (_text === '') {
         data.pinyin_proofread_word_list = [];
         return;
       }
-      data.text = text;
+      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;
 
@@ -340,7 +352,7 @@ export default {
             ),
           );
           this.$set(this.data, 'paragraph_list', mergedData);
-          this.paragraphVersion++;
+          this.paragraphVersion += 1;
           this.parseFClist(); // 确保在这里调用
         }
         this.$set(this.data, 'rich_text_list', rich_text.text_list);
@@ -349,9 +361,9 @@ export default {
     // 开启拼音之后,拼音样式要跟随标题样式
     createParsedTextStyleForTitle(styles) {
       if (!styles) return;
-      this.data.paragraph_list.forEach((outerArr, i) =>
-        outerArr.forEach((innerArr, j) =>
-          innerArr.forEach((newItem, k) => {
+      this.data.paragraph_list.forEach((outerArr) =>
+        outerArr.forEach((innerArr) =>
+          innerArr.forEach((newItem) => {
             if (!newItem.activeTextStyle) newItem.activeTextStyle = {};
             Object.assign(newItem.activeTextStyle, styles);
           }),

+ 6 - 9
src/views/book/courseware/create/index.vue

@@ -137,15 +137,12 @@ export default {
         this.pasteComponent('bottom');
       }
     },
-    judgeIsHasChange() {
-      if (this.isChange) {
-        this.visibleWarn = true;
-        return;
-      }
-      this.back();
-    },
-    back() {
-      this.$emit('goBackPreview');
+    /**
+     * 返回预览页
+     * @param {string} visible_id 编辑页最上方可见组件id
+     */
+    back(visible_id = '') {
+      this.$emit('goBackPreview', visible_id);
     },
     /**
      * 拖拽开始

+ 14 - 0
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -264,6 +264,7 @@ export default {
       visible: false,
       newpath: '',
       iframeHeight: `${window.innerHeight - 100}px`,
+      visible_id: this.$route.query?.visible_id || '', // 可见组件 id
     };
   },
   watch: {
@@ -292,12 +293,25 @@ export default {
       top: rect.top,
     };
     window.addEventListener('mousedown', this.handleMouseDown);
+    this.handleScrollVisibleComponent();
   },
   beforeDestroy() {
     window.removeEventListener('mousedown', this.handleMouseDown);
   },
   methods: {
     /**
+     * 滚动到 visible_id 的组件
+     */
+    async handleScrollVisibleComponent() {
+      if (!this.visible_id) return;
+      await this.$nextTick();
+
+      const target = await this.findChildComponentByKey(this.visible_id);
+      if (target && target.$el) {
+        target.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+      }
+    },
+    /**
      * 处理选中组件事件
      * 这个事件只在编辑预览模式下触发
      * @param {string} component_id 组件在课件内部的 ID

+ 5 - 8
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -29,12 +29,9 @@
                 <el-input
                   :key="j"
                   v-model="li.content"
-                  type="textarea"
-                  :autosize="{ minRows: 1, maxRows: 5 }"
-                  resize="none"
                   :disabled="disabled"
                   :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
-                  :style="[{ width: data.property.input_default_width + 'px' }]"
+                  :style="[{ width: Math.max(data.property.input_default_width, li.content.length * 21.3) + 'px' }]"
                 />
               </template>
 
@@ -57,7 +54,7 @@
                     :readonly="true"
                     :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
                     class="pinyin"
-                    :style="[{ width: data.property.input_default_width + 'px' }]"
+                    :style="[{ width: Math.max(data.property.input_default_width, li.content.length * 21.3) + 'px' }]"
                   />
                 </el-popover>
               </template>
@@ -468,7 +465,7 @@ export default {
       }
     }
 
-    .el-textarea {
+    .el-input {
       display: inline-flex;
       align-items: center;
       width: 120px;
@@ -499,11 +496,11 @@ export default {
         }
       }
 
-      :deep .el-textarea__inner {
+      :deep .el-input__inner {
         padding: 0;
         font-size: 16pt;
         color: $font-color;
-        text-align: left;
+        text-align: center;
         background-color: #fff;
         border-width: 0;
         border-bottom: 1px solid $font-color;

+ 6 - 2
src/views/personal_workbench/edit_task/edit/index.vue

@@ -268,7 +268,11 @@ export default {
     applySingleAttrToAllComponents({ attr, value }) {
       this.$refs.create.$refs.createCanvas.applySingleAttrToAllComponents(attr, value);
     },
-    goBackPreview() {
+    /**
+     * 返回预览页
+     * @param {string} visible_id 编辑页最上方可见组件id
+     */
+    goBackPreview(visible_id = '') {
       if (this.$route.query.template_type) {
         this.$router.push({
           path: `/personal_workbench/template_list/preview/${this.id}`,
@@ -277,7 +281,7 @@ export default {
       } else {
         this.$router.push({
           path: `/personal_workbench/edit_task/preview/${this.id}`,
-          query: { project_id: this.project_id },
+          query: { project_id: this.project_id, visible_id },
         });
       }
     },

+ 3 - 3
src/views/personal_workbench/edit_task/preview/CreateCoursewareAsTemplate.vue

@@ -51,9 +51,9 @@
           >
             <el-option v-for="(item, index) in options" :key="item.id" :label="item.name" :value="item.name">
               <span style="float: left">{{ item.name }}</span>
-              <span style="float: right; font-size: 12px; color: #8492a6" @click.stop="handleDeleteLabel(item, index)"
-                >删除</span
-              >
+              <span style="float: right; font-size: 12px; color: #8492a6" @click.stop="handleDeleteLabel(item, index)">
+                删除
+              </span>
             </el-option>
           </el-select>
         </div>

+ 3 - 1
src/views/personal_workbench/edit_task/preview/index.vue

@@ -106,9 +106,11 @@ export default {
       this.$router.push({ path: '/personal_workbench/edit_task' });
     },
     editTask() {
+      const visibleId = this.$refs.preview.getFirstVisibleComponentId();
+
       this.$router.push({
         path: `/personal_workbench/edit_task/edit/${this.$refs.preview.select_node}`,
-        query: { project_id: this.project_id },
+        query: { project_id: this.project_id, visible_id: visibleId },
       });
     },
     /**