Browse Source

Merge branch 'master' into lhd

natasha 2 days ago
parent
commit
511e94ee88

+ 3 - 0
src/components/CommonPreview.vue

@@ -1749,6 +1749,9 @@ export default {
     computedSelectedGroupCoursewareInfo() {
     computedSelectedGroupCoursewareInfo() {
       return this.$refs.courserware.computedSelectedGroupCoursewareInfo();
       return this.$refs.courserware.computedSelectedGroupCoursewareInfo();
     },
     },
+    saveCoursewareStyleTemplate(data) {
+      return this.$refs.courserware.saveCoursewareStyleTemplate(data);
+    },
 
 
     async submitChapterAllCoursewareToAuditFlow(chapter_id) {
     async submitChapterAllCoursewareToAuditFlow(chapter_id) {
       try {
       try {

+ 21 - 1
src/views/book/courseware/create/components/FullTextSettings.vue

@@ -15,6 +15,26 @@
           <el-color-picker v-model="unified_attrib.assist_color" />
           <el-color-picker v-model="unified_attrib.assist_color" />
           <span class="link" @click="generateAssistColor">自动生成辅助色</span>
           <span class="link" @click="generateAssistColor">自动生成辅助色</span>
         </div>
         </div>
+        <el-button
+          type="primary"
+          class="row-button"
+          @click="
+            applySingleAttrToSelectedComponents('topic_color');
+            applySingleAttrToSelectedComponents('assist_color');
+          "
+        >
+          应用选中组件
+        </el-button>
+        <el-button
+          type="primary"
+          class="row-button"
+          @click="
+            applySingleAttrToAllComponents('topic_color');
+            applySingleAttrToAllComponents('assist_color');
+          "
+        >
+          应用整页
+        </el-button>
       </el-form-item>
       </el-form-item>
       <el-form-item label="字体">
       <el-form-item label="字体">
         <el-select v-model="unified_attrib.font" placeholder="请选择字体">
         <el-select v-model="unified_attrib.font" placeholder="请选择字体">
@@ -222,7 +242,7 @@ export default {
   .el-form {
   .el-form {
     .color-group {
     .color-group {
       display: flex;
       display: flex;
-      column-gap: 10px;
+      column-gap: 6px;
       align-items: center;
       align-items: center;
 
 
       span {
       span {

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

@@ -473,6 +473,32 @@ export default {
       };
       };
     },
     },
     /**
     /**
+     * 保存课节为样式模板
+     * @param {object} param0 保存样式模板所需参数
+     * @param {string} param0.courseware_id 课件id
+     * @param {'true' | 'false'} param0.is_select_part_courseware_mode 是否选择部分课件模式
+     * @param {string[]} param0.component_id_list 组件id列表
+     */
+    async saveCoursewareStyleTemplate({ courseware_id, is_select_part_courseware_mode, component_id_list = [] }) {
+      const allComponentIds = this.data.row_list.flatMap((row) =>
+        row.col_list.flatMap((col) => col.grid_list.map((grid) => grid.id)),
+      );
+      // 如果是选择部分课件模式,则使用传入的 component_id_list,否则使用所有组件id列表
+      const idList = is_select_part_courseware_mode === 'true' ? component_id_list : allComponentIds;
+      const saveTasks = [];
+      // 遍历组件id列表,找到对应组件并调用其保存样式模板方法
+      for (const id of idList) {
+        const component = await this.findChildComponentByKey(id);
+        if (component && typeof component.saveStyleTemplate === 'function') {
+          saveTasks.push(component.saveStyleTemplate(courseware_id));
+        }
+      }
+
+      await Promise.all(saveTasks);
+
+      this.$message.success('已保存为样式模板');
+    },
+    /**
      * 清空行选择列表
      * 清空行选择列表
      */
      */
     clearRowCheckList() {
     clearRowCheckList() {

+ 21 - 1
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -5,7 +5,7 @@ import PreviewOperation from '@/views/book/courseware/preview/common/PreviewOper
 import AnswerCorrect from '@/views/book/courseware/preview/common/AnswerCorrect.vue';
 import AnswerCorrect from '@/views/book/courseware/preview/common/AnswerCorrect.vue';
 
 
 import { isEnable } from '@/views/book/courseware/data/common';
 import { isEnable } from '@/views/book/courseware/data/common';
-import { ContentGetCoursewareComponentContent } from '@/api/book';
+import { ContentGetCoursewareComponentContent, ContentSaveCoursewareComponentContent } from '@/api/book';
 import { sanitizeHTML } from '@/utils/common';
 import { sanitizeHTML } from '@/utils/common';
 
 
 const mixin = {
 const mixin = {
@@ -295,6 +295,26 @@ const mixin = {
       this.visibleAnswerCorrect = false;
       this.visibleAnswerCorrect = false;
       this.$set(this.data, 'answer_correct', correct);
       this.$set(this.data, 'answer_correct', correct);
     },
     },
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      if (noTextContentData.content) {
+        noTextContentData.content = '';
+      }
+      return noTextContentData;
+    },
+    /**
+     * @description 保存样式模板
+     * @param {string} courseware_id 课件id
+     */
+    saveStyleTemplate(courseware_id) {
+      const content = this.getNoTextContentData();
+      return ContentSaveCoursewareComponentContent({
+        courseware_id,
+        component_id: this.id,
+        component_type: this.data.type,
+        content: JSON.stringify(content),
+      });
+    },
   },
   },
 };
 };
 
 

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

@@ -390,6 +390,31 @@ export default {
       this.selectedWordList = [];
       this.selectedWordList = [];
       this.handleWav([]);
       this.handleWav([]);
     },
     },
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        model_essay: [],
+        content: '',
+        audio_file_id: '',
+        file_list: [],
+        multilingual: [],
+        analysis_list: [],
+        answer_list: [],
+        record_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 29 - 0
src/views/book/courseware/preview/components/judge/JudgePreview.vue

@@ -216,6 +216,35 @@ export default {
     retry() {
     retry() {
       this.answer.answer_list = [];
       this.answer.answer_list = [];
     },
     },
+
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        analysis_list: [],
+        answer_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      noTextContentData.option_list.forEach((item) => {
+        item.content = '';
+        item.paragraph_list = [];
+        item.paragraph_list_parameter = {
+          text: '',
+          pinyin_proofread_word_list: [],
+        };
+      });
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 32 - 0
src/views/book/courseware/preview/components/matching/MatchingPreview.vue

@@ -526,6 +526,38 @@ export default {
       );
       );
       this.$set(this, 'answer', { answer_list: [], is_right: false });
       this.$set(this, 'answer', { answer_list: [], is_right: false });
     },
     },
+
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        analysis_list: [],
+        answer_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      noTextContentData.option_list.forEach((list) => {
+        list.forEach((item) => {
+          item.content = '';
+          item.multilingual = [];
+          item.paragraph_list = [];
+          item.paragraph_list_parameter = {
+            text: '',
+            pinyin_proofread_word_list: [],
+          };
+        });
+      });
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 30 - 0
src/views/book/courseware/preview/components/select/SelectPreview.vue

@@ -192,6 +192,36 @@ export default {
     retry() {
     retry() {
       this.answer.answer_list = [];
       this.answer.answer_list = [];
     },
     },
+
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        analysis_list: [],
+        answer_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      noTextContentData.option_list.forEach((item) => {
+        item.content = '';
+        item.multilingual = [];
+        item.paragraph_list = [];
+        item.paragraph_list_parameter = {
+          text: '',
+          pinyin_proofread_word_list: [],
+        };
+      });
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 31 - 0
src/views/book/courseware/preview/components/sort/SortPreview.vue

@@ -203,6 +203,37 @@ export default {
       this.answer.answer_list = [];
       this.answer.answer_list = [];
       this.is_all_right = false;
       this.is_all_right = false;
     },
     },
+
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        analysis_list: [],
+        answer_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      noTextContentData.option_list.forEach((item) => {
+        item.content = '';
+        item.custom_serial_number = '';
+        item.multilingual = [];
+        item.paragraph_list = [];
+        item.paragraph_list_parameter = {
+          text: '',
+          pinyin_proofread_word_list: [],
+        };
+      });
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 47 - 0
src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue

@@ -590,6 +590,53 @@ export default {
       this.data.record_list = [];
       this.data.record_list = [];
       this.$refs.luyin?.handleReset();
       this.$refs.luyin?.handleReset();
     },
     },
+
+    /**
+     * 获取无文本内容的数据结构,用于保存为个人模板时的样式模板
+     */
+    getNoTextContentData() {
+      let noTextContentData = JSON.parse(JSON.stringify(this.data));
+      const resetFieldMap = {
+        lrc_arr: [],
+        lrc_data: {
+          name: '',
+          url: '',
+          id: '',
+          file_id: '',
+        },
+        audio_data: {
+          name: '',
+          media_duration: 0,
+          temporary_url: '',
+          url: '',
+          file_id: '',
+        },
+        analysis_list: [],
+        answer_list: [],
+        record_list: [],
+      };
+
+      Object.assign(noTextContentData, resetFieldMap);
+
+      noTextContentData.option_list.forEach((list) => {
+        list.forEach((grid) => {
+          grid.content = '';
+          grid.lrc_data = {
+            begin_time: 0,
+            end_time: 0,
+            text: '',
+          };
+          grid.multilingual = [];
+        });
+      });
+
+      if (noTextContentData.answer) {
+        noTextContentData.answer.answer_list = [];
+        noTextContentData.answer.reference_answer = '';
+      }
+
+      return noTextContentData;
+    },
   },
   },
 };
 };
 </script>
 </script>

+ 51 - 23
src/views/personal_workbench/edit_task/preview/CreateCoursewareAsTemplate.vue

@@ -1,10 +1,27 @@
 <template>
 <template>
-  <el-dialog :visible="visible" :title="title" :close-on-click-modal="false" width="420px" @close="dialogClose">
+  <el-dialog :visible="visible" title="保存为个人模板" :close-on-click-modal="false" width="420px" @close="dialogClose">
     <div class="form">
     <div class="form">
       <div class="form-item">
       <div class="form-item">
-        <label for="name">模板名称</label>
+        <label>范围</label>
         <div class="form-item-container">
         <div class="form-item-container">
-          <el-input id="name" v-model="form.name" placeholder="请输入模板名称" />
+          <el-radio-group v-model="scope">
+            <el-radio v-for="option in scopeOptions" :key="option.value" :label="option.value">
+              {{ option.label }}
+            </el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <div class="form-item">
+        <label>内容</label>
+        <div class="form-item-container">
+          <el-radio v-model="templateType" :disabled="contentDisabled" label="content">内容模板</el-radio>
+          <el-radio v-model="templateType" :disabled="contentDisabled" label="style">样式模板</el-radio>
+        </div>
+      </div>
+      <div class="form-item">
+        <label for="name">名称</label>
+        <div class="form-item-container">
+          <el-input id="name" v-model="form.name" placeholder="请输入名称" />
         </div>
         </div>
       </div>
       </div>
       <div class="form-item">
       <div class="form-item">
@@ -27,16 +44,9 @@
         </div>
         </div>
       </div>
       </div>
       <div class="form-item">
       <div class="form-item">
-        <label for="memo">模板描述</label>
+        <label for="memo">描述</label>
         <div class="form-item-container">
         <div class="form-item-container">
-          <el-input
-            id="memo"
-            v-model="form.memo"
-            placeholder="请输入模板描述"
-            type="textarea"
-            resize="none"
-            :rows="4"
-          />
+          <el-input id="memo" v-model="form.memo" placeholder="请输入描述" type="textarea" resize="none" :rows="4" />
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -56,30 +66,32 @@ export default {
       type: Boolean,
       type: Boolean,
       required: true,
       required: true,
     },
     },
-    scope: {
-      type: Number,
-      required: true,
-    },
   },
   },
   data() {
   data() {
     return {
     return {
-      titleList: {
-        0: '保存本页为模板',
-        1: '保存本书为模板',
-        3: '保存本课为模板',
-      },
       form: {
       form: {
         name: '',
         name: '',
         label_list: [],
         label_list: [],
         memo: '',
         memo: '',
+        scope: 0,
+        is_select_part_courseware_mode: 'false',
+        content_type: 0, // 0: 内容模板,1: 样式模板
       },
       },
       inputVisible: false,
       inputVisible: false,
       inputValue: '',
       inputValue: '',
+      scope: 0,
+      templateType: 'content',
+      scopeOptions: [
+        { label: '本页选中内容', value: -1 },
+        { label: '本页', value: 0 },
+        { label: '本课', value: 1 },
+        { label: '本书', value: 3 },
+      ],
     };
     };
   },
   },
   computed: {
   computed: {
-    title() {
-      return this.titleList[this.scope];
+    contentDisabled() {
+      return this.scope !== -1 && this.scope !== 0;
     },
     },
   },
   },
   watch: {
   watch: {
@@ -89,10 +101,26 @@ export default {
         name: '',
         name: '',
         label_list: [],
         label_list: [],
         memo: '',
         memo: '',
+        scope: 0,
+        is_select_part_courseware_mode: 'false',
+        content_type: 0,
       };
       };
       this.inputVisible = false;
       this.inputVisible = false;
       this.inputValue = '';
       this.inputValue = '';
     },
     },
+    scope(newVal) {
+      if (![0, -1].includes(newVal)) {
+        this.templateType = 'content';
+      }
+      this.form.scope = newVal;
+      this.form.is_select_part_courseware_mode = newVal === -1 ? 'true' : 'false'; // 只有选择本页选中内容时才是部分课件模式
+      if (newVal === -1) {
+        this.form.scope = 0;
+      }
+    },
+    templateType(newVal) {
+      this.form.content_type = newVal === 'content' ? 0 : 1;
+    },
   },
   },
   methods: {
   methods: {
     dialogClose() {
     dialogClose() {

+ 24 - 33
src/views/personal_workbench/edit_task/preview/index.vue

@@ -11,14 +11,7 @@
       @selectedComponent="curComponentData = $event"
       @selectedComponent="curComponentData = $event"
     >
     >
       <template #operator="{ courseware }">
       <template #operator="{ courseware }">
-        <el-popover placement="bottom" width="155" trigger="click">
-          <el-link type="primary" @click="openCreateTemplateDialog(0)">保存本页为模板</el-link>
-          <el-link type="primary" @click="openCreateTemplateDialog(3)">保存本课为模板</el-link>
-          <el-link type="primary" @click="openCreateTemplateDialog(1)">保存本书为模板</el-link>
-          <el-link type="primary" @click="openCreateTemplateDialog(0, 'true')">保存选择内容为模板</el-link>
-          <span slot="reference" class="link save-template">保存为个人模板</span>
-        </el-popover>
-        <span class="link"></span>
+        <span class="link" @click="openCreateTemplateDialog">保存为个人模板</span>
         <template v-if="isTrue(courseware.is_can_start_edit)">
         <template v-if="isTrue(courseware.is_can_start_edit)">
           <span class="link" @click="showSetBackground">背景设置</span>
           <span class="link" @click="showSetBackground">背景设置</span>
           <span class="link" @click="showSetComponentBackground">组件背景设置</span>
           <span class="link" @click="showSetComponentBackground">组件背景设置</span>
@@ -31,11 +24,7 @@
       </template>
       </template>
     </CommonPreview>
     </CommonPreview>
 
 
-    <CreateCoursewareAsTemplateVue
-      :visible.sync="visibleTemplate"
-      :scope="templateScope"
-      @confirm="saveCoursewareAsTemplate"
-    />
+    <CreateCoursewareAsTemplateVue :visible.sync="visibleTemplate" @confirm="saveCoursewareAsTemplate" />
 
 
     <SetBackground
     <SetBackground
       :visible.sync="visibleBackground"
       :visible.sync="visibleBackground"
@@ -88,8 +77,6 @@ export default {
       project_id: this.$route.query.project_id,
       project_id: this.$route.query.project_id,
       isTrue,
       isTrue,
       visibleTemplate: false,
       visibleTemplate: false,
-      templateScope: 0,
-      is_select_part_courseware_mode: 'false',
       background: {
       background: {
         background_image_url: '',
         background_image_url: '',
         background_position: {},
         background_position: {},
@@ -135,20 +122,19 @@ export default {
     },
     },
     /**
     /**
      * 打开保存为个人模板的弹窗
      * 打开保存为个人模板的弹窗
-     * @param {number} scope 模板范围 0-本页 1-本书 3-本课
-     * @param {'false' | 'true'} part_courseware_mode 是否部分课件模式 模板范围为 0 时有效
      */
      */
-    openCreateTemplateDialog(scope, part_courseware_mode = 'false') {
+    openCreateTemplateDialog() {
       this.visibleTemplate = true;
       this.visibleTemplate = true;
-      this.templateScope = scope;
-      this.is_select_part_courseware_mode = part_courseware_mode;
     },
     },
     /**
     /**
      * 保存为个人模板
      * 保存为个人模板
      * @param {object} form 模板信息
      * @param {object} form 模板信息
      * @param {string} form.name 模板名称
      * @param {string} form.name 模板名称
-     * @param {array} form.label_list 模板标签列表
+     * @param {string[]} form.label_list 模板标签列表
      * @param {string} form.memo 模板备注
      * @param {string} form.memo 模板备注
+     * @param {number} form.scope 模板范围
+     * @param {string} form.is_select_part_courseware_mode 是否选择部分课件模式
+     * @param {number} form.content_type 模板内容类型 0: 内容模板,1: 样式模板
      */
      */
     saveCoursewareAsTemplate(form) {
     saveCoursewareAsTemplate(form) {
       this.$confirm('确定保存为个人模板吗?', '提示', {
       this.$confirm('确定保存为个人模板吗?', '提示', {
@@ -159,22 +145,32 @@ export default {
         .then(() => {
         .then(() => {
           this.visibleTemplate = false;
           this.visibleTemplate = false;
           let courseware_info = {};
           let courseware_info = {};
-          if (this.is_select_part_courseware_mode === 'true') {
+          if (form.is_select_part_courseware_mode === 'true') {
             courseware_info = this.$refs.preview.computedSelectedGroupCoursewareInfo();
             courseware_info = this.$refs.preview.computedSelectedGroupCoursewareInfo();
-            if (Object.keys(courseware_info).length === 0) {
+
+            if (courseware_info?.component_id_list.length === 0) {
               this.$message.warning('请选择要保存的内容');
               this.$message.warning('请选择要保存的内容');
               return;
               return;
             }
             }
           }
           }
           SaveCoursewareAsTemplatePersonal({
           SaveCoursewareAsTemplatePersonal({
             courseware_id: this.$refs.preview.select_node,
             courseware_id: this.$refs.preview.select_node,
-            scope: this.templateScope,
-            is_select_part_courseware_mode: this.is_select_part_courseware_mode,
             courseware_info,
             courseware_info,
             ...form,
             ...form,
-          }).then(() => {
-            this.$message.success('已保存为个人模板');
-          });
+          })
+            .then(({ courseware_id }) => {
+              if (form.content_type === 0) {
+                return this.$message.success('已保存为个人模板');
+              }
+              this.$refs.preview.saveCoursewareStyleTemplate({
+                courseware_id,
+                is_select_part_courseware_mode: form.is_select_part_courseware_mode,
+                component_id_list: courseware_info?.component_id_list || [],
+              });
+            })
+            .catch(() => {
+              this.$message.error('保存个人模板失败');
+            });
         })
         })
         .catch(() => {});
         .catch(() => {});
     },
     },
@@ -264,11 +260,6 @@ export default {
 
 
 .task-preview {
 .task-preview {
   @include page-content(true);
   @include page-content(true);
-
-  .save-template {
-    margin-right: -8px;
-    vertical-align: text-top;
-  }
 }
 }
 </style>
 </style>