Просмотр исходного кода

Merge branch 'master' of http://gcls-git.helxsoft.cn/GCLS/eep_page

zq 6 дней назад
Родитель
Сommit
6d9c2edcbb
25 измененных файлов с 521 добавлено и 155 удалено
  1. 1 1
      .env
  2. 21 0
      src/api/project.js
  3. 83 0
      src/components/ColorPicker.vue
  4. 30 3
      src/components/CommonPreview.vue
  5. 41 0
      src/styles/element.scss
  6. 20 0
      src/utils/common.js
  7. 74 2
      src/views/book/courseware/create/components/CreateCanvas.vue
  8. 7 2
      src/views/book/courseware/create/components/FullTextSettings.vue
  9. 6 1
      src/views/book/courseware/create/components/SetBackground.vue
  10. 6 1
      src/views/book/courseware/create/components/SetComponentBackground.vue
  11. 22 10
      src/views/book/courseware/create/components/base/rich_text/RichText.vue
  12. 38 22
      src/views/book/courseware/create/components/question/table/Table.vue
  13. 6 9
      src/views/book/courseware/create/index.vue
  14. 1 1
      src/views/book/courseware/data/table.js
  15. 14 0
      src/views/book/courseware/preview/CoursewarePreview.vue
  16. 28 36
      src/views/book/courseware/preview/components/article/PhraseModelChs.vue
  17. 17 17
      src/views/book/courseware/preview/components/article/Practicechs.vue
  18. 2 2
      src/views/book/courseware/preview/components/article/components/AudioCompare.vue
  19. 66 13
      src/views/book/courseware/preview/components/article/components/AudioRed.vue
  20. 4 4
      src/views/book/courseware/preview/components/article/components/FreewriteLettle.vue
  21. 17 17
      src/views/book/courseware/preview/components/dialogue_article/Practicechs.vue
  22. 5 8
      src/views/book/courseware/preview/components/fill/FillPreview.vue
  23. 6 2
      src/views/personal_workbench/edit_task/edit/index.vue
  24. 3 3
      src/views/personal_workbench/edit_task/preview/CreateCoursewareAsTemplate.vue
  25. 3 1
      src/views/personal_workbench/edit_task/preview/index.vue

+ 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

@@ -1197,7 +1197,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');
           });
@@ -1229,7 +1229,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();
@@ -1718,7 +1718,7 @@ export default {
         .then(() => {
           this.allFeedbackList = this.allFeedbackList.filter((x) => x.id !== feedBackId);
         })
-        .catch((err) => {});
+        .catch(() => {});
     },
 
     getSearch(params) {
@@ -1793,6 +1793,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);
           }),

+ 38 - 22
src/views/book/courseware/create/components/question/table/Table.vue

@@ -144,7 +144,7 @@
         placeholder="请输入词汇,用于选词填空"
       />
       <p class="tips">在需要作答的单元格内输入三个以上下划线“___”</p>
-      <el-button @click="identifyText">识别</el-button>
+      <el-button @click="identifyText()">识别</el-button>
       <el-button @click="handleMultilingual">多语言</el-button>
       <template v-if="isEnable(data.has_identify)">
         <p class="tips">在需要作答的单元格内录入标准答案,多个填空答案用换行录入,同一个填空有多个答案用斜线“/”隔开</p>
@@ -188,21 +188,24 @@
           icon="el-icon-refresh"
           title="刷新"
           class="refresh-pinyin-btn"
-          @click.native="identifyText"
+          @click.native="identifyText()"
       /></el-divider>
-
-      <PinyinText
-        v-if="isEnable(data.property.view_pinyin)"
-        :id="'table_pinyin_text'"
-        ref="PinyinText"
-        :paragraph-list="data.paragraph_list"
-        :rich-text-list="data.rich_text_list"
-        :pinyin-position="data.property.pinyin_position"
-        :pinyin-size="data?.unified_attrib?.pinyin_size"
-        :font-size="data?.unified_attrib?.font_size"
-        :font-family="data?.unified_attrib?.font"
-        @fillCorrectPinyin="fillCorrectPinyin"
-      />
+      <template v-if="isEnable(data.property.view_pinyin)">
+        <template v-for="(item, index) in data.option_list">
+          <PinyinText
+            v-for="(items, indexs) in item"
+            :key="index + '_' + indexs"
+            :id="'table_pinyin_text_' + index + '_' + indexs"
+            ref="PinyinText"
+            :rich-text-list="items.rich_text_list"
+            :pinyin-position="data.property.pinyin_position"
+            :pinyin-size="data?.unified_attrib?.pinyin_size"
+            :font-size="data?.unified_attrib?.font_size"
+            :font-family="data?.unified_attrib?.font"
+            @fillCorrectPinyin="fillCorrectPinyin"
+          />
+        </template>
+      </template>
       <el-dialog
         title="配置单元格"
         :visible="editCellFlag"
@@ -335,7 +338,11 @@ export default {
           this.data.paragraph_list_parameter.text = text;
           this.data.paragraph_list_parameter.is_first_sentence_first_hz_pinyin_first_char_upper_case =
             this.data.property.is_first_sentence_first_hz_pinyin_first_char_upper_case;
-          this.createParsedTextInfoPinyin(text);
+          this.data.option_list.forEach((item, index) => {
+            item.forEach((items, indexs) => {
+              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+            });
+          });
         }
       },
       deep: true,
@@ -356,7 +363,11 @@ export default {
         if (text && isEnable(this.data.property.view_pinyin)) {
           this.data.paragraph_list_parameter.text = text;
           this.data.paragraph_list_parameter.is_first_sentence_first_hz_pinyin_first_char_upper_case = val;
-          this.createParsedTextInfoPinyin(text);
+          this.data.option_list.forEach((item, index) => {
+            item.forEach((items, indexs) => {
+              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+            });
+          });
         }
       },
       deep: true,
@@ -453,13 +464,18 @@ export default {
         if (editIndex) {
           let arr = editIndex.split('#');
           text = this.data.option_list[arr[0]][arr[1]].content;
+          this.createParsedTextInfoPinyin(text, editIndex);
+        } else {
+          this.data.option_list.forEach((item, index) => {
+            item.forEach((items, indexs) => {
+              this.createParsedTextInfoPinyin(items.content, index + '#' + indexs);
+            });
+          });
         }
-
-        this.createParsedTextInfoPinyin(text);
       }
     },
     // 获取拼音解析文本
-    createParsedTextInfoPinyin(text) {
+    createParsedTextInfoPinyin(text, editIndex) {
       if (text === '') {
         this.data.paragraph_list_parameter.pinyin_proofread_word_list = [];
         return;
@@ -490,8 +506,8 @@ export default {
             ),
           );
           this.data.paragraph_list = mergedData;
-          if (this.editContentIndex) {
-            let arr = this.editContentIndex.split('#');
+          if (editIndex) {
+            let arr = editIndex.split('#');
             let list = res.rich_text.text_list;
             list.forEach((item) => {
               let inputIndex = 0;

+ 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);
     },
     /**
      * 拖拽开始

+ 1 - 1
src/views/book/courseware/data/table.js

@@ -62,7 +62,7 @@ export function getOption() {
     content: '',
     mark: getRandomNumber(),
     model_essay: [],
-    
+    rich_text_list:[],
     cell: {
       bgColor: '',
       isCross: false,

+ 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

+ 28 - 36
src/views/book/courseware/preview/components/article/PhraseModelChs.vue

@@ -195,15 +195,13 @@
                                   ]"
                                   :style="{
                                     color:
-                                      pItem.chstimeList &&
-                                      pItem.chstimeList[wIndex] &&
-                                      curTime >= pItem.chstimeList[wIndex].wordBg &&
-                                      curTime < item.timeList[pItem.sentIndex].ed &&
-                                      attrib
-                                        ? attrib.topic_color
-                                        : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
-                                          ? pItem.matchNotesObj.notesColor
-                                          : pItem.config.color,
+                                      newWordList.indexOf(pItem.chs) > -1 || pItem.words
+                                        ? attrib
+                                          ? attrib.topic_color
+                                          : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
+                                            ? pItem.matchNotesObj.notesColor
+                                            : pItem.config.color
+                                        : pItem.config.color,
                                   }"
                                   @click.stop="viewNotes($event, pItem.chs[wIndex], pItem.chs, pItem)"
                                   >{{ convertText(pItem.chs[wIndex]) }}</span
@@ -258,15 +256,13 @@
                                   ]"
                                   :style="{
                                     color:
-                                      pItem.chstimeList &&
-                                      pItem.chstimeList[wIndex] &&
-                                      curTime >= pItem.chstimeList[wIndex].wordBg &&
-                                      curTime < item.timeList[pItem.sentIndex].ed &&
-                                      attrib
-                                        ? attrib.topic_color
-                                        : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
-                                          ? pItem.matchNotesObj.notesColor
-                                          : pItem.config.color,
+                                      newWordList.indexOf(pItem.chs) > -1 || pItem.words
+                                        ? attrib
+                                          ? attrib.topic_color
+                                          : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
+                                            ? pItem.matchNotesObj.notesColor
+                                            : pItem.config.color
+                                        : pItem.config.color,
                                   }"
                                   @click.stop="viewNotes($event, pItem.chs[wIndex], pItem.chs, pItem)"
                                   >{{ convertText(pItem.chs[wIndex]) }}</span
@@ -695,15 +691,13 @@
                                 ]"
                                 :style="{
                                   color:
-                                    pItem.chstimeList &&
-                                    pItem.chstimeList[wIndex] &&
-                                    curTime >= pItem.chstimeList[wIndex].wordBg &&
-                                    curTime < item.timeList[pItem.sentIndex].ed &&
-                                    attrib
-                                      ? attrib.topic_color
-                                      : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
-                                        ? pItem.matchNotesObj.notesColor
-                                        : pItem.config.color,
+                                    newWordList.indexOf(pItem.chs) > -1 || pItem.words
+                                      ? attrib
+                                        ? attrib.topic_color
+                                        : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
+                                          ? pItem.matchNotesObj.notesColor
+                                          : pItem.config.color
+                                      : pItem.config.color,
                                 }"
                                 @click.stop="viewNotes($event, pItem.chs[wIndex], pItem.chs, pItem)"
                                 >{{ convertText(pItem.chs[wIndex]) }}</span
@@ -757,15 +751,13 @@
                                 ]"
                                 :style="{
                                   color:
-                                    pItem.chstimeList &&
-                                    pItem.chstimeList[wIndex] &&
-                                    curTime >= pItem.chstimeList[wIndex].wordBg &&
-                                    curTime < item.timeList[pItem.sentIndex].ed &&
-                                    attrib
-                                      ? attrib.topic_color
-                                      : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
-                                        ? pItem.matchNotesObj.notesColor
-                                        : pItem.config.color,
+                                    newWordList.indexOf(pItem.chs) > -1 || pItem.words
+                                      ? attrib
+                                        ? attrib.topic_color
+                                        : pItem.matchNotesObj.con && pItem.matchNotesObj.notesColor
+                                          ? pItem.matchNotesObj.notesColor
+                                          : pItem.config.color
+                                      : pItem.config.color,
                                 }"
                                 @click.stop="viewNotes($event, pItem.chs[wIndex], pItem.chs, pItem)"
                                 >{{ convertText(pItem.chs[wIndex]) }}</span

+ 17 - 17
src/views/book/courseware/preview/components/article/Practicechs.vue

@@ -570,23 +570,23 @@
                   @sentPause="sentPause"
                   @handleWav="handleWav"
                 />
-                <!-- <div v-if="curQue.mp3_list && curQue.mp3_list.length > 0" class="compare-box">
-                <Audio-compare
-                  :theme-color="themeColor"
-                  :index="index"
-                  :sent-index="sentIndex"
-                  :url="curQue.mp3_list[0].id"
-                  :bg="curQue.wordTime[index].bg"
-                  :ed="curQue.wordTime[index].ed"
-                  :wavblob="wavblob"
-                  :get-cur-time="getCurTime"
-                  :sent-pause="sentPause"
-                  :is-record="isRecord"
-                  :handle-change-stop-audio="handleChangeStopAudio"
-                  :get-play-status="getPlayStatus"
-                  :attrib="attrib"
-                />
-              </div> -->
+                <div v-if="curQue.mp3_list && curQue.mp3_list.length > 0" class="compare-box">
+                  <Audio-compare
+                    :theme-color="themeColor"
+                    :index="index"
+                    :sent-index="sentIndex"
+                    :url="curQue.mp3_list[0].url"
+                    :bg="curQue.wordTime[index].bg"
+                    :ed="curQue.wordTime[index].ed"
+                    :wavblob="wavblob"
+                    :get-cur-time="getCurTime"
+                    :sent-pause="sentPause"
+                    :is-record="isRecord"
+                    :handle-change-stop-audio="handleChangeStopAudio"
+                    :get-play-status="getPlayStatus"
+                    :attrib="attrib"
+                  />
+                </div>
               </div>
               <span class="full-screen-icon" @click="fullScreen">
                 <svg-icon

+ 2 - 2
src/views/book/courseware/preview/components/article/components/AudioCompare.vue

@@ -106,8 +106,8 @@ export default {
 
 .compare-disable {
   display: block;
-  width: 24px;
-  height: 24px;
+  width: 20px;
+  height: 20px;
   margin-left: 8px;
 
   &-big {

+ 66 - 13
src/views/book/courseware/preview/components/article/components/AudioRed.vue

@@ -1,28 +1,57 @@
 <!--  -->
 <template>
   <div v-if="mp3" class="content-voices" @click="handlePlayVoice">
-    <img
-      :src="voiceSrc"
-      :class="type == 'full' ? 'icon-big' : ''"
-      class="icon-mask"
-      :style="{
-        backgroundColor: themeColor,
-        maskImage: `url(${voiceSrc})`,
-      }"
-    />
+    <template v-if="isCompare">
+      <div
+        class="icon-mask icon-compare-play"
+        :style="{
+          backgroundColor: attrib?.topic_color,
+        }"
+        :class="type == 'full' ? 'icon-big' : ''"
+        v-if="isPlaying"
+      ></div>
+      <div
+        class="icon-mask icon-compare-pause"
+        :style="{
+          backgroundColor: attrib?.topic_color,
+        }"
+        :class="type == 'full' ? 'icon-big' : ''"
+        v-else
+      ></div>
+    </template>
+    <template v-else>
+      <div
+        class="icon-mask icon-normal-play"
+        :style="{
+          backgroundColor: attrib?.topic_color,
+        }"
+        :class="type == 'full' ? 'icon-big' : ''"
+        v-if="isPlaying"
+      ></div>
+      <div
+        class="icon-mask icon-normal-pause"
+        :style="{
+          backgroundColor: attrib?.topic_color,
+        }"
+        :class="type == 'full' ? 'icon-big' : ''"
+        v-else
+      ></div>
+    </template>
+    <!-- <img :src="voiceSrc" :class="type == 'full' ? 'icon-big' : ''" class="icon-mask" :style="maskStyle" /> -->
   </div>
 </template>
 
 <script>
 export default {
   components: {},
-  props: ['seconds', 'mp3', 'wav', 'themeColor', 'isCompare', 'type', 'bg', 'ed'],
+  props: ['seconds', 'mp3', 'wav', 'themeColor', 'isCompare', 'type', 'bg', 'ed', 'attrib'],
   data() {
     return {
       audio: new Audio(),
       voiceSrc: '',
       voicePauseSrc: require('@/assets/play-red.png'),
       voicePlaySrc: require('@/assets/icon-voice-play-red.png'),
+      isPlaying: false,
     };
   },
   computed: {
@@ -32,6 +61,10 @@ export default {
     comparePlaySrc() {
       return require('@/assets/icon/pauseC-24-normal-red.png');
     },
+    // 通过 mask 把图标着成指定颜色
+    maskStyle() {
+      return {};
+    },
   },
   watch: {},
   // 生命周期 - 创建完成(可以访问当前this实例)
@@ -59,15 +92,18 @@ export default {
     _this.audio.addEventListener('play', function () {
       _this.voiceSrc = _this.isCompare ? _this.comparePlaySrc : _this.voicePlaySrc;
       _this.$emit('getPlayStatus', true);
+      _this.isPlaying = true;
     });
     _this.audio.addEventListener('pause', function () {
       _this.voiceSrc = _this.isCompare ? _this.comparePauseSrc : _this.voicePauseSrc;
       _this.$emit('getPlayStatus', false);
+      _this.isPlaying = false;
     });
     _this.audio.addEventListener('ended', function () {
       _this.voiceSrc = _this.isCompare ? _this.comparePauseSrc : _this.voicePauseSrc;
       _this.$emit('sentPause', false);
       _this.$emit('getPlayStatus', false);
+      _this.isPlaying = false;
     });
     _this.audio.addEventListener('timeupdate', function () {
       if (_this.ed) {
@@ -131,15 +167,32 @@ export default {
     }
   }
 
-  img {
+  img,
+  .icon-mask {
     float: left;
-    width: 16px;
-    height: 16px;
+    width: 20px;
+    height: 20px;
   }
 
   .icon-big {
     width: 24px;
     height: 24px;
   }
+
+  .icon-compare-play {
+    mask-image: url('@/assets/icon/pauseC-24-normal-red.png');
+  }
+
+  .icon-compare-pause {
+    mask-image: url('@/assets/compare-pause-red-24.png');
+  }
+
+  .icon-normal-play {
+    mask-image: url('@/assets/icon-voice-play-red.png');
+  }
+
+  .icon-normal-pause {
+    mask-image: url('@/assets/play-red.png');
+  }
 }
 </style>

+ 4 - 4
src/views/book/courseware/preview/components/article/components/FreewriteLettle.vue

@@ -89,10 +89,10 @@ export default {
       penIndex: 0,
       hanzicolor: '',
       hanziweight: '',
-      thinpen: require('../../../../assets/common/thin-pen.png'), // 细笔
-      thinpenActive: require('../../../../assets/common/thin-pen-active.png'),
-      thickpen: require('../../../../assets/common/thick-pen.png'),
-      thickpenActive: require('../../../../assets/common/thick-pen-active.png'),
+      thinpen: require('@/assets/thin-pen.png'), // 细笔
+      thinpenActive: require('@/assets/thin-pen-active.png'),
+      thickpen: require('@/assets/thick-pen.png'),
+      thickpenActive: require('@/assets/thick-pen-active.png'),
       collectFlag: false, // 是否收藏
       imgarr: [],
       imgOrCans: false,

+ 17 - 17
src/views/book/courseware/preview/components/dialogue_article/Practicechs.vue

@@ -659,23 +659,23 @@
                   @sentPause="sentPause"
                   @handleWav="handleWav"
                 />
-                <!-- <div v-if="curQue.mp3_list && curQue.mp3_list.length > 0" class="compare-box">
-                <Audio-compare
-                  :theme-color="attrib ? attrib.topic_color : '#e35454'"
-                  :index="index"
-                  :sent-index="sentIndex"
-                  :url="curQue.mp3_list[0].id"
-                  :bg="curQue.wordTime[index].bg"
-                  :ed="curQue.wordTime[index].ed"
-                  :wavblob="wavblob"
-                  :get-cur-time="getCurTime"
-                  :sent-pause="sentPause"
-                  :is-record="isRecord"
-                  :handle-change-stop-audio="handleChangeStopAudio"
-                  :get-play-status="getPlayStatus"
-                  :attrib="attrib"
-                />
-              </div> -->
+                <div v-if="curQue.mp3_list && curQue.mp3_list.length > 0" class="compare-box">
+                  <Audio-compare
+                    :theme-color="attrib ? attrib.topic_color : '#e35454'"
+                    :index="index"
+                    :sent-index="sentIndex"
+                    :url="curQue.mp3_list[0].url"
+                    :bg="curQue.wordTime[index].bg"
+                    :ed="curQue.wordTime[index].ed"
+                    :wavblob="wavblob"
+                    :get-cur-time="getCurTime"
+                    :sent-pause="sentPause"
+                    :is-record="isRecord"
+                    :handle-change-stop-audio="handleChangeStopAudio"
+                    :get-play-status="getPlayStatus"
+                    :attrib="attrib"
+                  />
+                </div>
               </div>
               <span class="full-screen-icon" @click="fullScreen">
                 <svg-icon

+ 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 },
       });
     },
     /**