Sfoglia il codice sorgente

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

zq 1 settimana fa
parent
commit
94282156b8
35 ha cambiato i file con 976 aggiunte e 591 eliminazioni
  1. 0 1
      src/api/book.js
  2. 122 14
      src/components/CommonPreview.vue
  3. 2 11
      src/components/PinyinText.vue
  4. 85 0
      src/components/Remark.vue
  5. 169 0
      src/components/SoundRecord.vue
  6. 73 73
      src/components/UploadFile.vue
  7. 47 84
      src/views/book/courseware/create/components/CreateCanvas.vue
  8. 61 67
      src/views/book/courseware/create/components/PreviewEdit.vue
  9. 1 0
      src/views/book/courseware/create/components/SelectBackground.vue
  10. 29 6
      src/views/book/courseware/create/components/base/common/SelectResource.vue
  11. 7 2
      src/views/book/courseware/create/components/common/ModuleMixin.js
  12. 7 1
      src/views/book/courseware/create/components/common/SelectUpload.vue
  13. 94 54
      src/views/book/courseware/create/components/question/article/NewWord.vue
  14. 3 1
      src/views/book/courseware/create/components/question/fill/Fill.vue
  15. 3 3
      src/views/book/courseware/create/components/question/image_text/ImageText.vue
  16. 2 1
      src/views/book/courseware/create/components/question/judge/Judge.vue
  17. 2 1
      src/views/book/courseware/create/components/question/matching/Matching.vue
  18. 75 32
      src/views/book/courseware/create/components/question/new_word/NewWord.vue
  19. 20 22
      src/views/book/courseware/create/components/question/new_word/NewWordSetting.vue
  20. 2 1
      src/views/book/courseware/create/components/question/select/Select.vue
  21. 2 1
      src/views/book/courseware/create/components/question/sort/Sort.vue
  22. 22 61
      src/views/book/courseware/create/components/question/video_interaction/VideoInteraction.vue
  23. 85 119
      src/views/book/courseware/preview/CoursewarePreview.vue
  24. 6 0
      src/views/book/courseware/preview/common/AnswerAnalysis.vue
  25. 2 0
      src/views/book/courseware/preview/components/fill/FillPreview.vue
  26. 4 2
      src/views/book/courseware/preview/components/judge/JudgePreview.vue
  27. 2 1
      src/views/book/courseware/preview/components/matching/MatchingPreview.vue
  28. 5 10
      src/views/book/courseware/preview/components/record_input/SoundRecord.vue
  29. 4 2
      src/views/book/courseware/preview/components/select/SelectPreview.vue
  30. 1 0
      src/views/book/courseware/preview/components/sort/SortPreview.vue
  31. 1 0
      src/views/book/courseware/preview/components/video_interaction/ExercisePreview.vue
  32. 16 16
      src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue
  33. 17 1
      src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue
  34. 3 3
      src/views/book/courseware/preview/components/voice_matrix/components/AudioCompareMatrix.vue
  35. 2 1
      src/views/personal_workbench/project/components/UploadFile.vue

+ 0 - 1
src/api/book.js

@@ -161,7 +161,6 @@ export function MangerAddResource(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_resource_manager-AddResource`, data);
 }
 
-
 /**
  * @description 得到课件路径
  * @param {object} data

+ 122 - 14
src/components/CommonPreview.vue

@@ -13,10 +13,28 @@
           <span class="link" @click="createCoursewarePreviewURL()">生成课件预览链接</span>
         </template>
         <div class="group">
-          <el-checkbox v-model="isShowGroup">显示分组</el-checkbox>
-          <el-checkbox v-model="groupShowAll">分组显示全部</el-checkbox>
           <el-checkbox v-model="isJudgeCorrect">判断对错</el-checkbox>
         </div>
+        <div v-if="isShowGroup">
+          <span class="link" @click="isShowGroup = false">取消显示分组</span>
+          <span
+            class="link"
+            @click="
+              groupShowAll = false;
+              isShowGroup = false;
+            "
+            >完成选择</span
+          >
+        </div>
+        <span
+          v-else
+          class="link"
+          @click="
+            isShowGroup = true;
+            groupShowAll = true;
+          "
+          >显示分组</span
+        >
         <span class="link">
           <el-checkbox v-model="chinese" true-label="zh-Hant" false-label="zh-Hans">繁体</el-checkbox>
         </span>
@@ -79,6 +97,13 @@
 
         <main :class="['preview-main', { 'no-audit': !isShowAudit }]">
           <div class="preview-left"></div>
+          <!-- <div
+            id="selectable-area-preview"
+            @mousedown="startSelection"
+            @mousemove="updateSelection"
+            @mouseup="endSelection"
+            @mouseleave="endSelection"
+          > -->
           <CoursewarePreview
             v-if="courseware_info.book_name"
             ref="courserware"
@@ -98,6 +123,19 @@
             @editNote="handEditNote"
             @saveCollect="saveCollect"
           />
+          <!-- <div
+              v-if="isSelecting"
+              :style="{
+                position: 'absolute',
+                top: `${menuPosition.startY}px`,
+                left: `${menuPosition.startX}px`,
+                width: `${menuPosition.endX - menuPosition.startX}px`,
+                height: `${menuPosition.endY - menuPosition.startY}px`,
+                border: '2px solid #165DFF',
+              }"
+            ></div>
+          </div> -->
+
           <div class="preview-right"></div>
         </main>
 
@@ -334,13 +372,7 @@
       class="remark-dialog"
       @close="dialogClose('')"
     >
-      <RichText
-        v-model="remark_content"
-        toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright image media link"
-        :wordlimit-num="false"
-        :height="240"
-        page-from="audit"
-      />
+      <Remark :remark="remark" :project-id="projectId" :courseware-id="select_node ? select_node : id" />
       <div slot="footer">
         <el-button @click="dialogClose('')">取消</el-button>
         <el-button type="primary" :loading="submit_loading" @click="addCoursewareAuditRemark(select_node)">
@@ -397,7 +429,6 @@
 
 <script>
 import CoursewarePreview from '@/views/book/courseware/preview/CoursewarePreview.vue';
-import RichText from '@/components/RichText.vue';
 import MindMap from '@/components/MindMap.vue';
 import VisNetwork from '@/components/VisNetwork.vue';
 import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
@@ -406,6 +437,7 @@ import AuditRemark from '@/components/AuditRemark.vue';
 import ExplanatoryNoteDialog from '@/components/ExplanatoryNoteDialog.vue';
 import SimAnswerPermissionControl from '@/components/SimAnswerPermissionControl.vue';
 import PreviewURL from '@/views/project_manage/common/PreviewURL.vue';
+import Remark from './Remark.vue';
 
 import {
   GetBookCoursewareInfo,
@@ -442,7 +474,6 @@ export default {
   name: 'CommonPreview',
   components: {
     CoursewarePreview,
-    RichText,
     MindMap,
     AudioPlay,
     VideoPlay,
@@ -451,6 +482,7 @@ export default {
     VisNetwork,
     SimAnswerPermissionControl,
     PreviewURL,
+    Remark,
   },
   provide() {
     return {
@@ -550,14 +582,23 @@ export default {
       searchList: [],
       searchContent: '',
       visible: false,
-      remark_content: '',
+      remark: {
+        remark_content: '',
+        file_id_list: [],
+      },
       submit_loading: false,
       isTrue,
       menuPosition: {
         x: -1,
         y: -1,
         componentId: 'WHOLE',
+        startX: null,
+        startY: null,
+        endX: null,
+        endY: null,
       },
+      isSelecting: false,
+
       curToolbarIcon: this.isShowAudit ? 'audit' : '',
       sidebarIconList,
       twoSidebarList: [],
@@ -854,7 +895,10 @@ export default {
       });
     },
     addRemark(selectNode, x, y, componentId, content_select) {
-      this.remark_content = '';
+      this.remark = {
+        remark_content: '',
+        file_id_list: [],
+      };
       this.visible = true;
       if (selectNode) {
         this.menuPosition = {
@@ -880,11 +924,12 @@ export default {
       this.submit_loading = true;
       AddCoursewareAuditRemark({
         courseware_id: id || this.id,
-        content: this.remark_content,
+        content: this.remark.remark_content,
         component_id: this.menuPosition.componentId,
         position_x: this.menuPosition.x,
         position_y: this.menuPosition.y,
         content_select: this.menuPosition.content_select,
+        file_id_list: this.remark.file_id_list,
       })
         .then(() => {
           this.submit_loading = false;
@@ -1455,6 +1500,69 @@ export default {
     computedSelectedGroupCoursewareInfo() {
       return this.$refs.courserware.computedSelectedGroupCoursewareInfo();
     },
+    startSelection(event) {
+      if (
+        this.isTrue(this.courseware_info.is_my_audit_task) &&
+        this.isTrue(this.courseware_info.is_can_add_audit_remark)
+      ) {
+        this.isSelecting = true;
+
+        let clientRect = document.getElementById(`selectable-area-preview`).getBoundingClientRect();
+        this.menuPosition.startX = event.clientX - clientRect.left;
+        this.menuPosition.startY = event.clientY - clientRect.top;
+      }
+    },
+    updateSelection(event) {
+      if (!this.isSelecting) return;
+      let clientRect = document.getElementById(`selectable-area-preview`).getBoundingClientRect();
+
+      this.menuPosition.endX = event.clientX - clientRect.left;
+      this.menuPosition.endY = event.clientY - clientRect.top;
+    },
+    endSelection() {
+      console.log('this.startX' + this.menuPosition.startX);
+      console.log('this.endX' + this.menuPosition.endX);
+      console.log('this.startY' + this.menuPosition.startY);
+      console.log('this.endY' + this.menuPosition.endY);
+
+      if (!this.isSelecting || this.menuPosition.startX === this.menuPosition.endX || !this.menuPosition.endX) return;
+      this.isSelecting = false;
+      const width = Math.abs(this.menuPosition.endX - this.menuPosition.startX);
+      const height = Math.abs(this.menuPosition.endY - this.menuPosition.startY);
+      const x =
+        this.menuPosition.endX > this.menuPosition.startX
+          ? `${this.menuPosition.startX}px`
+          : `${this.menuPosition.endX}px`;
+      const y =
+        this.menuPosition.endY > this.menuPosition.startY
+          ? `${this.menuPosition.startY}px`
+          : `${this.menuPosition.endY}px`;
+      let obj = {
+        id: Math.random().toString(36).substring(2, 10),
+        width: `${width}px`,
+        height: `${height}px`,
+        x,
+        y,
+        text: '',
+      };
+      this.menuPosition.startX = null;
+      this.menuPosition.endX = null;
+      this.menuPosition.startY = null;
+      this.menuPosition.endY = null;
+      if (width && height && this.isText) {
+        this.data.text_list.push(obj);
+        this.activeType = 'text';
+        this.activeIndex = this.data.text_list.length - 1;
+
+        this.hotspotsActiveIndex = this.data.text_list.length - 1;
+      } else if (width && height && !this.isText) {
+        this.data.input_list.push(obj);
+        this.activeType = 'input';
+        this.activeIndex = this.data.input_list.length - 1;
+
+        this.inputActiveIndex = this.data.input_list.length - 1;
+      }
+    },
   },
 };
 </script>

+ 2 - 11
src/components/PinyinText.vue

@@ -131,12 +131,10 @@ export default {
     paragraphList: {
       type: Array,
       default: () => [],
-      required: false,
     },
     richTextList: {
       type: Array,
       default: () => [],
-      required: false,
     },
     pinyinPosition: {
       type: String,
@@ -152,7 +150,7 @@ export default {
     },
     pinyinSize: {
       type: String,
-      default: '',
+      default: '12pt',
     },
     pinyinOverallPosition: {
       type: String,
@@ -276,7 +274,7 @@ export default {
           blocks.push({
             type: 'newline',
           });
-          paragraphIndex++;
+          paragraphIndex += 1;
         } else if (item.word_list && item.word_list.length > 0) {
           // 处理文字内容:将当前标签栈中的样式应用到文字块
           // 合并当前所有打开标签的样式
@@ -324,13 +322,6 @@ export default {
       immediate: true,
       deep: true,
     },
-    richTextList: {
-      handler(val) {
-        console.log('richTextList', JSON.stringify(val));
-      },
-      immediate: true,
-      deep: true,
-    },
   },
 
   methods: {

+ 85 - 0
src/components/Remark.vue

@@ -0,0 +1,85 @@
+<template>
+  <div class="remark-wrapper">
+    <RichText
+      v-model="remark.remark_content"
+      toolbar="fontselect fontsizeselect forecolor backcolor | underline | bold italic strikethrough alignleft aligncenter alignright image media link"
+      :wordlimit-num="false"
+      :height="240"
+      page-from="audit"
+    />
+    <SoundRecord @updateFileList="updateFileList"></SoundRecord>
+    <UploadFile
+      key="upload_remark"
+      :total-size="200"
+      :file-list="file_list"
+      :file-id-list="remark.file_id_list"
+      :label-text="labelText"
+      :accept-file-type="acceptFileType"
+      :icon-class="iconClass"
+      :limit="100"
+      :single-size="200"
+      :projectId="projectId"
+      :coursewareId="coursewareId"
+      :upload-tip="uploadTip"
+      @updateFileList="updateFileLists"
+    />
+  </div>
+</template>
+
+<script>
+import RichText from '@/components/RichText.vue';
+import SoundRecord from './SoundRecord.vue';
+import UploadFile from './UploadFile.vue';
+export default {
+  name: 'Remark',
+  components: { RichText, SoundRecord, UploadFile },
+  props: {
+    remark: {
+      type: Object,
+      default: {},
+    },
+    projectId: {
+      type: String,
+      default: '',
+    },
+    // 课件id
+    coursewareId: {
+      type: String,
+      default: '',
+    },
+  },
+  data() {
+    return {
+      file_list: [],
+      labelText: '',
+      acceptFileType: '.png,.jpg,.jpeg,.txt,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.mp3,.mp4,.zip,.rar',
+      uploadTip: '支持上传jpg、png、mp3、mp4、zip、txt、doc、docx、xsl、xslx、pdf、ppt等格式文件',
+      iconClass: '',
+    };
+  },
+  computed: {},
+  watch: {},
+  mounted() {},
+  methods: {
+    updateFileList(id) {
+      this.remark.file_id_list.push(id);
+      this.file_list.push({
+        id,
+        file_name: '录制音频',
+      });
+    },
+    updateFileLists({ file_list, file_id_list, file_info_list, file_info }) {
+      this.file_list = file_list;
+      this.remark.file_id_list = file_id_list;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.remark-wrapper {
+  :deep .sound-record-wrapper {
+    margin: 20px 0;
+  }
+}
+</style>

+ 169 - 0
src/components/SoundRecord.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="sound-record-wrapper">
+    <div class="sound-microphone" @click="microphone">
+      <img v-if="microphoneStatus" :src="require('@/assets/fill/record-ing.png')" class="voice-play" />
+      <SvgIcon v-else icon-class="mic-line" class="record" />
+      <span class="auto-btn">录制音频</span>
+      <span>{{ secondFormatConversion(recordTimes) }}</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import Recorder from 'js-audio-recorder'; // 录音插件
+import { secondFormatConversion } from '@/utils/transform';
+import { GetStaticResources, GetFileStoreInfo } from '@/api/app';
+export default {
+  name: 'SoundRecord',
+  props: {},
+  data() {
+    return {
+      secondFormatConversion,
+      recorder: new Recorder({
+        sampleBits: 16, // 采样位数,支持 8 或 16,默认是16
+        sampleRate: 16000, // 采样率,支持 11025、16000、22050、24000、44100、48000,根据浏览器默认值,我的chrome是48000
+        numChannels: 1, // 声道,支持 1 或 2, 默认是1
+      }),
+      timer: null, // 计时器
+      microphoneStatus: false, // 是否录音
+      hasMicro: '', // 录音后的样式class
+      audio: {
+        paused: true,
+      },
+      playtime: 0, // 播放时间
+      recordTimes: 0,
+      file_url: '',
+      recordTime: 0,
+    };
+  },
+  computed: {},
+  watch: {},
+  mounted() {
+    this.$refs.audio.addEventListener('ended', () => {
+      this.audio.paused = true;
+    });
+    this.$refs.audio.addEventListener('pause', () => {
+      this.audio.paused = true;
+    });
+    this.$refs.audio.addEventListener('play', () => {
+      this.audio.paused = false;
+    });
+  },
+  methods: {
+    // 开始录音
+    microphone() {
+      let _this = this;
+      if (this.microphoneStatus) {
+        this.handleStop();
+      } else {
+        this.hasMicro = '';
+        // 开始录音
+        this.recorder.start();
+        this.microphoneStatus = true;
+        this.recordTimes = 0;
+        clearInterval(this.timer);
+        this.timer = setInterval(() => {
+          // 每条录音不能超过10分钟
+          if (_this.recordTimes < 600) {
+            _this.recordTimes += 1;
+          } else {
+            _this.handleStop();
+          }
+        }, 1000);
+      }
+    },
+    handleStop() {
+      this.hasMicro = 'normal';
+      this.recorder.stop();
+      clearInterval(this.timer);
+      // 录音结束,获取取录音数据
+      let wav = this.recorder.getWAVBlob(); // 获取 WAV 数据
+      this.microphoneStatus = false;
+      let reader = new window.FileReader();
+      reader.readAsDataURL(wav);
+      reader.onloadend = () => {
+        let MethodName = 'file_store_manager-SaveFileByteBase64Text';
+        let data = {
+          base64_text: reader.result.replace('data:audio/wav;base64,', ''),
+          file_suffix_name: 'mp3',
+        };
+        GetStaticResources(MethodName, data).then((res) => {
+          if (res.status === 1) {
+            this.$emit('updateFileList', res.file_id);
+          }
+        });
+      };
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.sound-record-wrapper {
+  display: flex;
+  column-gap: 12px;
+  align-items: center;
+  width: 200px;
+  padding: 5px 12px;
+  background: $fill-color;
+  border-radius: 2px;
+
+  .audio-play-btn {
+    cursor: pointer;
+
+    &.not-url {
+      color: #a1a1a1;
+      cursor: not-allowed;
+    }
+  }
+
+  .record-time {
+    flex: 1;
+    min-width: 52px;
+    font-size: 14px;
+    font-weight: 400;
+    line-height: 22px;
+    color: #a1a1a1;
+
+    &.record-ing {
+      color: #000;
+    }
+  }
+
+  .record {
+    cursor: pointer;
+  }
+
+  .voice-play {
+    width: 16px;
+    height: 16px;
+  }
+
+  .delete-btn {
+    margin-left: 12px;
+    color: #4e4e4e;
+    cursor: pointer;
+
+    &.not-url {
+      color: #a1a1a1;
+      cursor: not-allowed;
+    }
+  }
+
+  .auto-btn {
+    font-size: 16px;
+    font-weight: 400;
+    line-height: 22px;
+    color: #1d2129;
+    cursor: pointer;
+  }
+
+  .sound-microphone {
+    display: flex;
+    column-gap: 12px;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+  }
+}
+</style>

+ 73 - 73
src/components/UploadFile.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="upload-file">
     <div class="file-area">
-      <span class="label-text">{{ labelText }}</span>
+      <span class="label-text" v-if="labelText">{{ labelText }}</span>
       <div class="upload-box">
         <el-upload
           ref="upload"
@@ -16,14 +16,14 @@
           :on-exceed="handleExceed"
           :limit="limit"
         >
-          <el-button>{{ type === 'h5_games' ? '选择Zip压缩包或单个html文件' : '选取' + labelText + '文件' }}</el-button>
+          <el-button>上传附件</el-button>
         </el-upload>
         <el-button size="small" type="primary" @click="uploadFiles"> 上传 </el-button>
         <el-button size="small" type="primary" @click="useResource"> 使用资源 </el-button>
       </div>
     </div>
-    <el-divider v-if="showDivider" />
     <div class="upload-tip">{{ uploadTip }}</div>
+    <el-divider v-if="showDivider" />
     <ul v-if="fileList.length > 0" slot="file-list" class="file-list">
       <li v-for="(file, i) in fileList" :key="i">
         <div class="file-name">
@@ -72,12 +72,12 @@
         </template>
       </li>
     </ul>
-
     <SelectResource
       :visible.sync="visibleResource"
       :project-id="projectId"
       :accept="accept"
       :courseware-id="coursewareId"
+      from-page="remark"
       @selectResource="selectResource"
     />
   </div>
@@ -202,12 +202,12 @@ export default {
   computed: {
     accept() {
       let accept = '*';
-      if (this.acceptFileType.includes('.mp3')) {
-        accept = 'audio';
+      if (this.acceptFileType.includes('.jpg')) {
+        accept = 'image';
       } else if (this.acceptFileType.includes('.mp4')) {
         accept = 'video';
-      } else if (this.acceptFileType.includes('.jpg')) {
-        accept = 'image';
+      } else if (this.acceptFileType.includes('.mp3')) {
+        accept = 'audio';
       } else if (this.acceptFileType.includes('.zip')) {
         accept = 'h5_game';
       } else if (this.acceptFileType.includes('.txt')) {
@@ -404,6 +404,8 @@ export default {
             }
           });
         });
+      // 选中文件后自动上传
+      this.uploadFiles();
     },
 
     // 显示弹窗
@@ -446,98 +448,96 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.module-content {
-  .file-area {
-    display: flex;
-    column-gap: 16px;
-    align-items: center;
+.file-area {
+  display: flex;
+  column-gap: 16px;
+  align-items: center;
 
-    .label-text {
-      font-size: 14px;
-      color: $font-light-color;
-    }
-
-    div {
-      flex: 1;
-    }
+  .label-text {
+    font-size: 14px;
+    color: $font-light-color;
   }
 
-  .el-divider {
-    margin: 16px 0;
+  div {
+    flex: 1;
   }
+}
 
-  .upload-tip {
-    margin-bottom: 16px;
-    font-size: 12px;
-    color: #86909c;
-  }
+.el-divider {
+  margin: 16px 0;
+}
 
-  .upload-box {
-    display: flex;
-    justify-content: space-between;
+.upload-tip {
+  margin-bottom: 16px;
+  font-size: 12px;
+  color: #86909c;
+}
 
-    .el-button + .el-button {
-      margin-left: 2px;
-    }
+.upload-box {
+  display: flex;
+  justify-content: space-between;
 
-    .file-uploader {
-      flex: 1;
+  .el-button + .el-button {
+    margin-left: 2px;
+  }
 
-      :deep .el-upload {
-        &--text {
-          width: 100%;
-          background-color: $fill-color;
-          border-radius: 2px 0 0 2px;
+  .file-uploader {
+    flex: 1;
 
-          .el-button {
-            width: 100%;
-            color: #86909c;
-            text-align: left;
-          }
+    :deep .el-upload {
+      &--text {
+        width: 100%;
+        background-color: $fill-color;
+        border-radius: 2px 0 0 2px;
+
+        .el-button {
+          width: 100%;
+          color: #86909c;
+          text-align: left;
         }
       }
     }
-
-    .el-button {
-      border-radius: 0 2px 2px 0;
-    }
   }
 
-  .old_file_list {
-    margin-top: 16px;
+  .el-button {
+    border-radius: 0 2px 2px 0;
   }
+}
 
-  .file-list {
+.old_file_list {
+  margin-top: 16px;
+}
+
+.file-list {
+  display: flex;
+  flex-direction: column;
+  row-gap: 16px;
+
+  li {
     display: flex;
-    flex-direction: column;
-    row-gap: 16px;
+    column-gap: 12px;
+    align-items: center;
 
-    li {
+    .file-name {
       display: flex;
-      column-gap: 12px;
+      column-gap: 14px;
       align-items: center;
+      justify-content: space-between;
+      max-width: 500px; // 360px有点窄
+      padding: 8px 12px;
+      font-size: 14px;
+      color: #1d2129;
+      background-color: #f7f8fa;
 
-      .file-name {
+      span {
         display: flex;
         column-gap: 14px;
         align-items: center;
-        justify-content: space-between;
-        max-width: 500px; // 360px有点窄
-        padding: 8px 12px;
-        font-size: 14px;
-        color: #1d2129;
-        background-color: #f7f8fa;
-
-        span {
-          display: flex;
-          column-gap: 14px;
-          align-items: center;
-        }
       }
+    }
 
-      .svg-icon {
-        cursor: pointer;
-      }
+    .svg-icon {
+      cursor: pointer;
     }
   }
 }

+ 47 - 84
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -838,106 +838,69 @@ export default {
       }
     },
     computedColStyle(col) {
-      let grid = col.grid_list;
+      const grid = col.grid_list || [];
 
-      let maxCol = 0; // 最大列数
-      let rowList = new Map();
-      grid.forEach(({ row }) => {
-        rowList.set(row, (rowList.get(row) || 0) + 1);
-      });
-      let curMaxRow = 0; // 当前列数最多的 row 的值
-      rowList.forEach((value, key) => {
-        if (value > maxCol) {
-          maxCol = value;
-          curMaxRow = key;
+      // 只分组一次,后续复用,避免在循环中反复 filter/find。
+      const rowMap = new Map();
+      const rowOrder = [];
+      let maxCol = 0;
+      let curMaxRow = 0;
+
+      grid.forEach((item) => {
+        if (!rowMap.has(item.row)) {
+          rowMap.set(item.row, []);
+          rowOrder.push(item.row);
+        }
+        const rowItems = rowMap.get(item.row);
+        rowItems.push(item);
+        if (rowItems.length > maxCol) {
+          maxCol = rowItems.length;
+          curMaxRow = item.row;
         }
       });
+
       // 计算 grid_template_areas
-      let gridStr = '';
-      let gridArr = [];
-      let gridRowArr = []; // 存储不同 row 中间的 line
-      grid.forEach(({ grid_area, row }, i) => {
-        // 如果 gridArr[row - 1] 不存在则创建
-        if (!gridArr[row - 1]) {
-          gridArr[row - 1] = [];
+      const areaLines = [];
+      rowOrder.forEach((row, index) => {
+        const rowItems = rowMap.get(row) || [];
+        if (index > 0 && row > 1) {
+          areaLines.push(`'${`middle-${rowItems[0].grid_area} `.repeat(maxCol * 3)}'`);
         }
 
-        if (row > 1 && grid[i - 1].row !== row) {
-          gridRowArr.push({
-            row: row - 1,
-            str: `middle-${grid_area} `.repeat(maxCol * 3),
+        const rowAreas = [];
+        if (row === curMaxRow) {
+          rowItems.forEach(({ grid_area }) => {
+            rowAreas.push(`left-${grid_area} ${grid_area} right-${grid_area}`);
           });
-        }
-
-        // 如果当前 row 是最大列数的 row
-        if (curMaxRow === row) {
-          gridArr[row - 1].push(`left-${grid_area} ${grid_area} right-${grid_area}`);
         } else {
-          let filter = grid.filter((item) => item.row === row);
-          let find = filter.findIndex((item) => item.grid_area === grid_area);
-          let needNum = (maxCol - filter.length) * 3; // 需要的数量
-
-          let str = '';
-          if (filter.length === 1) {
-            str = ` ${grid_area} `.repeat(needNum + 1);
-          } else {
-            let arr = this.splitInteger(needNum, filter.length);
-            str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
-          }
-          gridArr[row - 1].push(`left-${grid_area} ${str} right-${grid_area}`);
-        }
-      });
-      gridArr.forEach((item, i) => {
-        let find = gridRowArr.find((row) => row.row === i);
-        if (find) {
-          gridStr += `'${find.str}' `;
-        }
-        gridStr += `'${item.join(' ')}' `;
-      });
-      // 计算 grid_template_columns
-      let gridTemCols = '';
-      let max = { row: 0, num: 0 };
-      grid.forEach(({ row }) => {
-        // 计算出 row 的哪个值最多
-        let len = grid.filter((item) => item.row === row).length;
-        if (max.num < len) {
-          max.num = len;
-          max.row = row;
-        }
-      });
-      grid.forEach((item) => {
-        if (item.row === max.row) {
-          gridTemCols += `${item.width} 4px 4px `;
+          const needNum = (maxCol - rowItems.length) * 3;
+          const spread = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
+          rowItems.forEach(({ grid_area }, i) => {
+            const repeatNum = rowItems.length === 1 ? needNum + 1 : spread[i] + 1;
+            const inner = ` ${grid_area} `.repeat(repeatNum);
+            rowAreas.push(`left-${grid_area} ${inner} right-${grid_area}`);
+          });
         }
+        areaLines.push(`'${rowAreas.join(' ')}'`);
       });
 
+      // 计算 grid_template_columns(使用列数最多的一行作为基准)
+      const maxRowItems = rowMap.get(curMaxRow) || [];
+      const gridTemCols = maxRowItems.map(({ width }) => width).join(' 4px 4px ');
+
       // 计算 grid_template_rows
-      let gridTemplateRows = '';
-      // 将 grid 按照 row 分组
-      let gridMap = new Map();
-      grid.forEach((item) => {
-        if (!gridMap.has(item.row)) {
-          gridMap.set(item.row, []);
-        }
-        gridMap.get(item.row).push(item?.edit_height || item.height);
-      });
-      gridMap.forEach((value, i) => {
-        if (i > 1) {
-          gridTemplateRows += '4px ';
-        }
-        if (value.length === 1) {
-          gridTemplateRows += `${value[0]} `;
-        } else {
-          let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
-          gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
-        }
+      const rowHeights = rowOrder.map((row) => {
+        const heights = (rowMap.get(row) || []).map((item) => item?.edit_height || item.height);
+        if (heights.length <= 1) return heights[0] || 'auto';
+        const isAllAuto = heights.every((item) => item === 'auto');
+        return isAllAuto ? 'auto' : `max(${heights.join(', ')})`;
       });
 
       return {
         width: col.width,
-        gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${gridStr} '${'grid-bottom '.repeat(maxCol * 3)}'`,
-        gridTemplateColumns: `0 ${gridTemCols.slice(0, gridTemCols.length - 8)} 0`,
-        gridTemplateRows: `0 ${gridTemplateRows} 0`,
+        gridTemplateAreas: `'${'grid-top '.repeat(maxCol * 3)}' ${areaLines.join(' ')} '${'grid-bottom '.repeat(maxCol * 3)}'`,
+        gridTemplateColumns: `0 ${gridTemCols} 0`,
+        gridTemplateRows: `0 ${rowHeights.join(' 4px ')} 0`,
       };
     },
     /**

+ 61 - 67
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -1,5 +1,5 @@
 <template>
-  <div ref="preview" class="preview">
+  <div ref="previewRoot" class="preview">
     <div v-if="heightPrompt" class="height-prompt"></div>
     <template v-for="(row, i) in rowList">
       <!-- 行 -->
@@ -22,7 +22,7 @@
                   :class="[type, ...lineClass]"
                   :style="{ gridArea: type }"
                   :data-type="type"
-                  @mousedown="dragStart($event, { cursor, type: type, i, j, k, id: grid.id })"
+                  @mousedown="dragStart($event, { cursor, type, i, j, k, id: grid.id })"
                 ></span>
               </template>
               <component
@@ -30,7 +30,6 @@
                 :id="grid.id"
                 ref="preview"
                 :key="`preview-${grid.id}`"
-                @handleHeightChange="handleHeightChange"
                 :courseware-id="coursewareId"
                 type="edit"
                 :class="[grid.id]"
@@ -39,6 +38,7 @@
                   height: grid.height,
                   overflow: 'auto',
                 }"
+                @handleHeightChange="handleHeightChange"
               />
             </div>
           </div>
@@ -105,6 +105,7 @@ export default {
           lineClass: ['drag-line'],
         },
       ],
+      dragElement: null, // 当前拖拽的组件实例
       // 不需要移动的组件
       noMoveComponent: ['divider', 'spacing'],
       bookInfo: {
@@ -126,7 +127,7 @@ export default {
     document.addEventListener('mouseup', this.dragEnd);
   },
   mounted() {
-    const element = this.$refs.preview;
+    const element = this.$refs.previewRoot;
     // 监听 courserware 高度变化,获取其高度
     this.resizeObserver = new ResizeObserver(() => {
       const rect = element.getBoundingClientRect();
@@ -176,82 +177,69 @@ export default {
       }
       return arr;
     },
+    /**
+     * 计算列样式
+     * @param {Object} col 列对象
+     */
     computedColStyle(col) {
       const grid = col.grid_list;
 
-      let maxCol = 0; // 最大列数
-      let rowList = new Map();
-      grid.forEach(({ row }) => {
-        rowList.set(row, (rowList.get(row) || 0) + 1);
+      // 单次分组:后续 areas / columns / rows 统一复用 rowGroups。
+      const rowGroups = new Map();
+      grid.forEach((item) => {
+        if (!rowGroups.has(item.row)) {
+          rowGroups.set(item.row, []);
+        }
+        rowGroups.get(item.row).push(item);
       });
+
+      let maxCol = 0; // 最大列数
       let curMaxRow = 0; // 当前数量最大 row 的值
-      rowList.forEach((value, key) => {
-        if (value > maxCol) {
-          maxCol = value;
-          curMaxRow = key;
+      rowGroups.forEach((items, row) => {
+        if (items.length > maxCol) {
+          maxCol = items.length;
+          curMaxRow = row;
         }
       });
+
+      const sortedRows = Array.from(rowGroups.keys()).sort((a, b) => a - b);
+
       // 计算 grid_template_areas
       let gridTemplateAreas = '';
-      let gridArr = [];
-      grid.forEach(({ grid_area, row }) => {
-        if (!gridArr[row - 1]) {
-          gridArr[row - 1] = [];
-        }
-        if (curMaxRow === row) {
-          gridArr[row - 1].push(`${grid_area}`);
-        } else {
-          let filter = grid.filter((item) => item.row === row);
-          let find = filter.findIndex((item) => item.grid_area === grid_area);
-          let needNum = maxCol - filter.length; // 需要的数量
+      sortedRows.forEach((row) => {
+        const rowItems = rowGroups.get(row) || [];
+        const needNum = maxCol - rowItems.length; // 需要补齐的数量
+        const splitArr = rowItems.length > 1 ? this.splitInteger(needNum, rowItems.length) : [];
 
-          let str = '';
-          if (filter.length === 1) {
-            str = ` ${grid_area} `.repeat(needNum + 1);
-          } else {
-            let arr = this.splitInteger(needNum, filter.length);
-            str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
+        const areaList = rowItems.map(({ grid_area }, index) => {
+          if (curMaxRow === row) {
+            return `${grid_area}`;
           }
-          gridArr[row - 1].push(`${str}`);
-        }
-      });
-      gridArr.forEach((item) => {
-        gridTemplateAreas += `'${item.join(' ')}' `;
+          if (rowItems.length === 1) {
+            return ` ${grid_area} `.repeat(needNum + 1);
+          }
+          return splitArr[index] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(splitArr[index] + 1);
+        });
+
+        gridTemplateAreas += `'${areaList.join(' ')}' `;
       });
 
       // 计算 grid_template_columns
       let gridTemplateColumns = '';
-      let max = { row: 0, num: 0 };
-      grid.forEach(({ row }) => {
-        // 计算出 row 的哪个值最多
-        let len = grid.filter((item) => item.row === row).length;
-        if (max.num < len) {
-          max.num = len;
-          max.row = row;
-        }
-      });
-      grid.forEach((item) => {
-        if (item.row === max.row) {
-          gridTemplateColumns += `${item.width} `;
-        }
+      const maxRowItems = rowGroups.get(curMaxRow) || [];
+      maxRowItems.forEach((item) => {
+        gridTemplateColumns += `${item.width} `;
       });
 
       // 计算 grid_template_rows
       let gridTemplateRows = '';
-      // 将 grid 按照 row 分组
-      let gridMap = new Map();
-      grid.forEach((item) => {
-        if (!gridMap.has(item.row)) {
-          gridMap.set(item.row, []);
-        }
-        gridMap.get(item.row).push(item.height);
-      });
-      gridMap.forEach((value) => {
-        if (value.length === 1) {
-          gridTemplateRows += `${value[0]} `;
+      sortedRows.forEach((row) => {
+        const heights = (rowGroups.get(row) || []).map((item) => item.height);
+        if (heights.length === 1) {
+          gridTemplateRows += `${heights[0]} `;
         } else {
-          let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
-          gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
+          const isAllAuto = heights.every((item) => item === 'auto'); // 是否全是 auto
+          gridTemplateRows += isAllAuto ? 'auto ' : `max(${heights.join(', ')}) `;
         }
       });
 
@@ -273,6 +261,10 @@ export default {
      * @param {number} k
      */
     dragStart(event, { cursor, type, id, i, j, k }) {
+      const dragElement = this.findChildComponentByKey(`preview-${id}`);
+      if (!dragElement) return;
+      this.dragElement = dragElement;
+
       const { clientX, clientY } = event;
       this.drag = {
         dragging: true,
@@ -285,7 +277,6 @@ export default {
         k,
       };
 
-      this.bgColor = '#272727';
       document.body.style.cursor = cursor;
     },
     /**
@@ -300,11 +291,10 @@ export default {
       const offsetX = clientX - startX;
       const offsetY = clientY - startY;
 
-      let el = this.findChildComponentByKey(`preview-${id}`);
-      let { min_height, min_width } = el.data;
+      let { min_height, min_width } = this.dragElement.data;
+
+      const ROW_WIDTH = 1000;
 
-      // 获取行的宽度
-      const row_width = document.getElementsByClassName(`row-${i}`)[0].getBoundingClientRect().width;
       this.$emit('computedMoveData', {
         i,
         j,
@@ -315,7 +305,7 @@ export default {
         id,
         min_width,
         min_height,
-        row_width,
+        row_width: ROW_WIDTH,
       });
 
       this.drag.startX = clientX;
@@ -332,9 +322,13 @@ export default {
         startX: 0,
         startY: 0,
         type: '',
+        id: '',
+        i: -1,
+        j: -1,
+        k: -1,
       };
 
-      this.bgColor = '#ebebeb';
+      this.dragElement = null;
       document.body.style.cursor = 'auto';
     },
     // 获取子组件

+ 1 - 0
src/views/book/courseware/create/components/SelectBackground.vue

@@ -2,6 +2,7 @@
   <el-dialog
     custom-class="background"
     width="608px"
+    append-to-body
     :close-on-click-modal="false"
     :visible="visible"
     :before-close="handleClose"

+ 29 - 6
src/views/book/courseware/create/components/base/common/SelectResource.vue

@@ -12,12 +12,25 @@
         <el-input v-model="search_content" />
         <el-button type="primary" @click="queryList()">查询</el-button>
       </div>
-      <div class="search-right">
-        <label>排序:</label>
-        <el-select v-model="sort_value" placeholder="请选择">
-          <el-option v-for="item in sort_list" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-        <SvgIcon :icon-class="isDesc ? 'sort-down' : 'sort-up'" size="16" style="cursor: pointer" @click="changeSort" />
+      <div class="search-content-box">
+        <div class="search-right" v-if="fromPage">
+          <label>类型:</label>
+          <el-select v-model="type_index" placeholder="请选择" @change="queryList()">
+            <el-option v-for="item in type_list" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+        <div class="search-right">
+          <label>排序:</label>
+          <el-select v-model="sort_value" placeholder="请选择">
+            <el-option v-for="item in sort_list" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+          <SvgIcon
+            :icon-class="isDesc ? 'sort-down' : 'sort-up'"
+            size="16"
+            style="cursor: pointer"
+            @click="changeSort"
+          />
+        </div>
       </div>
     </div>
 
@@ -133,6 +146,11 @@ export default {
       type: String,
       default: '*',
     },
+    // 父级页面
+    fromPage: {
+      type: String,
+      default: '',
+    },
   },
   data() {
     const type_list = [
@@ -348,6 +366,11 @@ export default {
     justify-content: space-between;
     padding: 5px;
 
+    .search-content-box {
+      display: flex;
+      gap: 10px;
+    }
+
     .search-left {
       display: flex;
       flex-flow: wrap;

+ 7 - 2
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -294,14 +294,15 @@ const mixin = {
         return;
       }
 
-      data.paragraph_list_parameter.text = text.replace(/&nbsp;/g, ' ');
+      data.paragraph_list_parameter.text = text;
 
       PinyinBuild_OldFormat({
         ...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,
         is_fill_space: 'true',
-      }).then(({ parsed_text }) => {
+        is_rich_text: 'true',
+      }).then(({ parsed_text, rich_text }) => {
         if (parsed_text) {
           // 合并 activeTextStyle
           const mergedData = parsed_text.paragraph_list.map((outerArr, i) =>
@@ -320,6 +321,10 @@ const mixin = {
           );
           data.paragraph_list = mergedData;
         }
+
+        if (rich_text) {
+          data.rich_text_list = rich_text.text_list;
+        }
       });
     },
     /**

+ 7 - 1
src/views/book/courseware/create/components/common/SelectUpload.vue

@@ -15,7 +15,7 @@
       >
         <el-button>{{ showText }}</el-button>
       </el-upload>
-      <el-button size="small" class="upload-button" type="primary" @click="uploadFiles">上传</el-button>
+      <el-button size="small" class="upload-button" type="primary" @click="uploadFiles">本地上传</el-button>
       <el-button v-if="isShowResource" size="small" type="primary" @click="useResource">使用资源</el-button>
     </div>
     <SelectResource
@@ -102,6 +102,12 @@ export default {
   methods: {
     onFileChange(file, fileList) {
       this.fileList = fileList;
+      fileList.forEach((file) => {
+        if (!file.progress || file.progress <= 0) file.progress = 0;
+      });
+
+      // 选中文件后自动上传
+      this.uploadFiles();
     },
     uploadFiles() {
       const list = this.$refs.upload.uploadFiles;

+ 94 - 54
src/views/book/courseware/create/components/question/article/NewWord.vue

@@ -201,13 +201,16 @@
       </div>
     </div>
     <el-button icon="el-icon-plus" style="margin: 24px 0" @click="addElement">增加一个</el-button>
-    <SelectUpload
-      label="生词音频"
-      type="audio"
-      width="500px"
-      :style="{ marginBottom: data.audio_data.url.length === 0 ? '5px' : '' }"
-      @uploadSuccess="uploadAudioSuccess"
-    />
+    <div class="btn-box">
+      <SelectUpload
+        label="生词音频"
+        type="audio"
+        width="500px"
+        :style="{ marginBottom: data.audio_data.url.length === 0 ? '5px' : '' }"
+        @uploadSuccess="uploadAudioSuccess"
+      />
+      <el-button :loading="autoLoading" @click="handleAutoAudio">自动生成音频</el-button>
+    </div>
     <div v-if="data.audio_data.url.length > 0" class="upload-file">
       <div class="file-name">
         <span>
@@ -264,28 +267,26 @@
         </el-select>
       </el-form-item>
 
-      <template v-if="data.property.audio_generation_method === 'auto'">
-        <el-form-item label="音色">
-          <el-select v-model="data.property.voice_type" placeholder="请选择">
-            <el-option
-              v-for="{ voice_type, name } in voice_type_list"
-              :key="voice_type"
-              :label="name"
-              :value="voice_type"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="风格情感">
-          <el-select v-model="data.property.emotion">
-            <el-option v-for="{ emotion, name } in emotion_list" :key="emotion" :label="name" :value="emotion" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="语速">
-          <el-select v-model="data.property.speed_ratio">
-            <el-option v-for="{ value, label } in speedRatioList" :key="value" :label="label" :value="value" />
-          </el-select>
-        </el-form-item>
-      </template>
+      <el-form-item label="音色">
+        <el-select v-model="data.property.voice_type" placeholder="请选择">
+          <el-option
+            v-for="{ voice_type, name } in voice_type_list"
+            :key="voice_type"
+            :label="name"
+            :value="voice_type"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="风格情感">
+        <el-select v-model="data.property.emotion">
+          <el-option v-for="{ emotion, name } in emotion_list" :key="emotion" :label="name" :value="emotion" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="语速">
+        <el-select v-model="data.property.speed_ratio">
+          <el-option v-for="{ value, label } in speedRatioList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
 
       <el-form-item label="拼音位置">
         <el-radio-group v-model="data.property.pinyin_position">
@@ -354,6 +355,7 @@ export default {
       voice_type_list: [],
       emotion_list: [],
       speedRatioList,
+      autoLoading: false,
     };
   },
   methods: {
@@ -423,30 +425,30 @@ export default {
           file_id,
         };
         this.data.file_id_list.push(file_id);
-        let _this = this;
-        _this.loading = true;
-        let data = {
-          file_id,
-        };
-        fileToBase64Text(data)
-          .then((res) => {
-            let taskIddata = {
-              fileName: name,
-              speechBase64: res.base64_text,
-              language: 'ch',
-            };
-            prepareTranscribe(taskIddata)
-              .then((reses) => {
-                _this.loading = false;
-                _this.$set(_this.data, 'taskId', reses.data.taskId);
-              })
-              .catch(() => {
-                _this.loading = false;
-              });
-          })
-          .catch(() => {
-            _this.loading = false;
-          });
+        // let _this = this;
+        // _this.loading = true;
+        // let data = {
+        //   file_id,
+        // };
+        // fileToBase64Text(data)
+        //   .then((res) => {
+        //     let taskIddata = {
+        //       fileName: name,
+        //       speechBase64: res.base64_text,
+        //       language: 'ch',
+        //     };
+        //     prepareTranscribe(taskIddata)
+        //       .then((reses) => {
+        //         _this.loading = false;
+        //         _this.$set(_this.data, 'taskId', reses.data.taskId);
+        //       })
+        //       .catch(() => {
+        //         _this.loading = false;
+        //       });
+        //   })
+        //   .catch(() => {
+        //     _this.loading = false;
+        //   });
       }
     },
     getBase64(file) {
@@ -588,7 +590,7 @@ export default {
       row.pinyin = cnchar.spell(cons, 'array', 'low', 'tone').join(' ');
     },
     createWordTimes() {
-      if (this.data.taskId) {
+      if (this.data.audio_data.file_id) {
         let text = '';
         this.data.new_word_list.forEach((sItem, index) => {
           // item.forEach((sItem, sIndex) => {
@@ -696,6 +698,38 @@ export default {
         })
         .catch(() => {});
     },
+    // 自动生成音频
+    handleAutoAudio() {
+      this.autoLoading = true;
+      let text = '';
+      this.data.new_word_list.forEach((sItem, index) => {
+        // item.forEach((sItem, sIndex) => {
+        text += `${sItem.new_word}。`;
+        // });
+      });
+      TextToAudioFile({
+        text: text,
+        voice_type: this.data.property.voice_type,
+        emotion: this.data.property.emotion,
+        speed_ratio: this.data.property.speed_ratio,
+      })
+        .then(({ status, file_id, file_url }) => {
+          this.autoLoading = false;
+          if (status === 1) {
+            this.data.audio_data = {
+              name: '自动生成课文音频.mp3',
+              media_duration: 0,
+              temporary_url: file_url,
+              url: file_id,
+              file_id,
+            };
+            this.data.file_id_list.push(file_id);
+          }
+        })
+        .catch(() => {
+          this.autoLoading = false;
+        });
+    },
   },
   created() {
     this.getTextToAudioConfParamList();
@@ -783,6 +817,12 @@ export default {
     margin: 0;
   }
 }
+
+.btn-box {
+  display: flex;
+  flex-flow: wrap;
+  gap: 10px;
+}
 </style>
 <style lang="scss">
 .tox .tox-editor-header {

+ 3 - 1
src/views/book/courseware/create/components/question/fill/Fill.vue

@@ -71,6 +71,7 @@
               :key="`${i}-${j}`"
               ref="PinyinText"
               :paragraph-list="li.paragraph_list"
+              :rich-text-list="li.rich_text_list"
               :pinyin-position="data.property.pinyin_position"
               @fillCorrectPinyin="fillCorrectPinyin($event, i, j, 'model_essay')"
             />
@@ -321,7 +322,8 @@ export default {
 
       this.data.model_essay.forEach((item, i) => {
         item.forEach((option, j) => {
-          const text = option.content.replace(/<[^>]+>/g, '');
+          const text = option.content;
+
           if (!text) return;
           option.paragraph_list_parameter.text = text;
           this.createParsedTextInfoPinyin(text, i, j, 'model_essay');

+ 3 - 3
src/views/book/courseware/create/components/question/image_text/ImageText.vue

@@ -25,7 +25,7 @@
         />
       </div>
       <SelectUpload label="音频" type="audio" width="500px" @uploadSuccess="uploadAudioSuccess" />
-      <div v-if="data.mp3_list.length > 0" class="upload-file">
+      <div v-if="data.mp3_list.length > 0" class="upload-files">
         <div class="file-name">
           <span>
             <SvgIcon icon-class="mp3" size="12" />
@@ -654,8 +654,8 @@ export default {
 };
 </script>
 <style lang="scss" scoped>
-.upload-file {
-  // display: flex;
+.upload-files {
+  display: flex;
   column-gap: 12px;
   align-items: center;
   margin: 8px 0;

+ 2 - 1
src/views/book/courseware/create/components/question/judge/Judge.vue

@@ -47,6 +47,7 @@
         :key="i"
         ref="PinyinText"
         :paragraph-list="item.paragraph_list"
+        :rich-text-list="item.rich_text_list"
         :pinyin-position="data.property.pinyin_position"
         @fillCorrectPinyin="fillCorrectPinyin($event, i)"
       />
@@ -109,7 +110,7 @@ export default {
         }
         if (this.data.option_list.length > 0 && this.data.option_list[0].paragraph_list.length > 0) return;
         this.data.option_list.forEach((item, i) => {
-          const text = item.content.replace(/<[^>]+>/g, '');
+          const text = item.content;
           if (!text) return;
           item.paragraph_list_parameter.text = text;
           this.createParsedTextInfoPinyin(text, i);

+ 2 - 1
src/views/book/courseware/create/components/question/matching/Matching.vue

@@ -52,6 +52,7 @@
             :key="`${i}-${j}`"
             ref="PinyinText"
             :paragraph-list="li.paragraph_list"
+            :rich-text-list="li.rich_text_list"
             :pinyin-position="data.property.pinyin_position"
             @fillCorrectPinyin="fillCorrectPinyin($event, i, j)"
           />
@@ -193,7 +194,7 @@ export default {
 
         this.data.option_list.forEach((item, i) => {
           item.forEach((option, j) => {
-            const text = option.content.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ');
+            const text = option.content;
             if (!text) return;
             option.paragraph_list_parameter.text = text;
             this.createParsedTextInfoPinyin(text, i, j);

+ 75 - 32
src/views/book/courseware/create/components/question/new_word/NewWord.vue

@@ -346,13 +346,17 @@
       </div>
 
       <el-button icon="el-icon-plus" style="margin: 24px 0" @click="addElement">增加一个</el-button>
-      <SelectUpload
-        label="生词音频"
-        type="audio"
-        width="500px"
-        :style="{ marginBottom: data.audio_data.url.length === 0 ? '5px' : '' }"
-        @uploadSuccess="uploadAudioSuccess"
-      />
+      <div class="btn-box">
+        <SelectUpload
+          label="生词音频"
+          type="audio"
+          width="500px"
+          :style="{ marginBottom: data.audio_data.url.length === 0 ? '5px' : '' }"
+          @uploadSuccess="uploadAudioSuccess"
+        />
+        <el-button :loading="autoLoading" @click="handleAutoAudio">自动生成音频</el-button>
+      </div>
+
       <div v-if="data.audio_data.url.length > 0" class="upload-file">
         <div class="file-name">
           <span>
@@ -439,6 +443,7 @@ export default {
       multilingualText: '',
       visible: false,
       subtitleList: [],
+      autoLoading: false,
     };
   },
   watch: {
@@ -496,30 +501,30 @@ export default {
           file_id,
         };
         this.data.file_id_list.push(file_id);
-        let _this = this;
-        _this.loading = true;
-        let data = {
-          file_id,
-        };
-        fileToBase64Text(data)
-          .then((res) => {
-            let taskIddata = {
-              fileName: name,
-              speechBase64: res.base64_text,
-              language: 'ch',
-            };
-            prepareTranscribe(taskIddata)
-              .then((reses) => {
-                _this.loading = false;
-                _this.$set(_this.data, 'taskId', reses.data.taskId);
-              })
-              .catch(() => {
-                _this.loading = false;
-              });
-          })
-          .catch(() => {
-            _this.loading = false;
-          });
+        // let _this = this;
+        // _this.loading = true;
+        // let data = {
+        //   file_id,
+        // };
+        // fileToBase64Text(data)
+        //   .then((res) => {
+        //     let taskIddata = {
+        //       fileName: name,
+        //       speechBase64: res.base64_text,
+        //       language: 'ch',
+        //     };
+        //     prepareTranscribe(taskIddata)
+        //       .then((reses) => {
+        //         _this.loading = false;
+        //         _this.$set(_this.data, 'taskId', reses.data.taskId);
+        //       })
+        //       .catch(() => {
+        //         _this.loading = false;
+        //       });
+        //   })
+        //   .catch(() => {
+        //     _this.loading = false;
+        //   });
       }
     },
     getBase64(file) {
@@ -718,7 +723,7 @@ export default {
       this.data.source_courseware_id = val[val.length - 1];
     },
     createWordTimes() {
-      if (this.data.taskId) {
+      if (this.data.audio_data.file_id) {
         let text = '';
         this.data.new_word_list.forEach((sItem, index) => {
           // item.forEach((sItem, sIndex) => {
@@ -838,6 +843,38 @@ export default {
         .filter((item) => item.length > 0)
         .join(' ');
     },
+    // 自动生成音频
+    handleAutoAudio() {
+      this.autoLoading = true;
+      let text = '';
+      this.data.new_word_list.forEach((sItem, index) => {
+        // item.forEach((sItem, sIndex) => {
+        text += `${sItem.new_word}。`;
+        // });
+      });
+      TextToAudioFile({
+        text: text,
+        voice_type: this.data.property.voice_type,
+        emotion: this.data.property.emotion,
+        speed_ratio: this.data.property.speed_ratio,
+      })
+        .then(({ status, file_id, file_url }) => {
+          this.autoLoading = false;
+          if (status === 1) {
+            this.data.audio_data = {
+              name: '自动生成课文音频.mp3',
+              media_duration: 0,
+              temporary_url: file_url,
+              url: file_id,
+              file_id,
+            };
+            this.data.file_id_list.push(file_id);
+          }
+        })
+        .catch(() => {
+          this.autoLoading = false;
+        });
+    },
   },
 };
 </script>
@@ -927,6 +964,12 @@ export default {
     margin: 0;
   }
 }
+
+.btn-box {
+  display: flex;
+  flex-flow: wrap;
+  gap: 10px;
+}
 </style>
 <style lang="scss">
 .tox .tox-editor-header {

+ 20 - 22
src/views/book/courseware/create/components/question/new_word/NewWordSetting.vue

@@ -21,28 +21,26 @@
           <el-option v-for="{ value, label } in audioGenerationMethodList" :key="value" :label="label" :value="value" />
         </el-select>
       </el-form-item>
-      <template v-if="property.audio_generation_method === 'auto'">
-        <el-form-item label="音色">
-          <el-select v-model="property.voice_type" placeholder="请选择">
-            <el-option
-              v-for="{ voice_type, name } in voice_type_list"
-              :key="voice_type"
-              :label="name"
-              :value="voice_type"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="风格情感">
-          <el-select v-model="property.emotion">
-            <el-option v-for="{ emotion, name } in emotion_list" :key="emotion" :label="name" :value="emotion" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="语速">
-          <el-select v-model="property.speed_ratio">
-            <el-option v-for="{ value, label } in speedRatioList" :key="value" :label="label" :value="value" />
-          </el-select>
-        </el-form-item>
-      </template>
+      <el-form-item label="音色">
+        <el-select v-model="property.voice_type" placeholder="请选择">
+          <el-option
+            v-for="{ voice_type, name } in voice_type_list"
+            :key="voice_type"
+            :label="name"
+            :value="voice_type"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="风格情感">
+        <el-select v-model="property.emotion">
+          <el-option v-for="{ emotion, name } in emotion_list" :key="emotion" :label="name" :value="emotion" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="语速">
+        <el-select v-model="property.speed_ratio">
+          <el-option v-for="{ value, label } in speedRatioList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
       <el-form-item label="拼音位置">
         <el-radio-group v-model="property.pinyin_position">
           <el-radio v-for="{ value, label } in pinyinPositionList" :key="value" :label="value">

+ 2 - 1
src/views/book/courseware/create/components/question/select/Select.vue

@@ -41,6 +41,7 @@
         :key="i"
         ref="PinyinText"
         :paragraph-list="item.paragraph_list"
+        :rich-text-list="item.rich_text_list"
         :pinyin-position="data.property.pinyin_position"
         @fillCorrectPinyin="fillCorrectPinyin($event, i)"
       />
@@ -106,7 +107,7 @@ export default {
         }
         if (this.data.option_list.length > 0 && this.data.option_list[0].paragraph_list.length > 0) return;
         this.data.option_list.forEach((item, i) => {
-          const text = item.content.replace(/<[^>]+>/g, '');
+          const text = item.content;
           if (!text) return;
           item.paragraph_list_parameter.text = text;
           this.createParsedTextInfoPinyin(text, i);

+ 2 - 1
src/views/book/courseware/create/components/question/sort/Sort.vue

@@ -49,6 +49,7 @@
         :key="i"
         ref="PinyinText"
         :paragraph-list="item.paragraph_list"
+        :rich-text-list="item.rich_text_list"
         :pinyin-position="data.property.pinyin_position"
         @fillCorrectPinyin="fillCorrectPinyin($event, i)"
       />
@@ -132,7 +133,7 @@ export default {
         }
         if (this.data.option_list.length > 0 && this.data.option_list[0].paragraph_list.length > 0) return;
         this.data.option_list.forEach((item, i) => {
-          const text = item.content.replace(/<[^>]+>/g, '');
+          const text = item.content;
           if (!text) return;
           item.paragraph_list_parameter.text = text;
           this.createParsedTextInfoPinyin(text, i);

+ 22 - 61
src/views/book/courseware/create/components/question/video_interaction/VideoInteraction.vue

@@ -18,8 +18,8 @@
       />
       <div v-if="data.video_list.length > 0 && data.video_list[0].media_duration" class="interaction-box">
         <video
-          ref="videoPlayer"
           id="interaction-video"
+          ref="videoPlayer"
           :src="data.video_list[0].file_url"
           width="100%"
           height="400"
@@ -28,7 +28,7 @@
         ></video>
         <!-- <video ref="videoPlayer" class="video-js vjs-default-skin vjs-big-play-centered"></video> -->
 
-        <el-button type="primary" size="small" @click="handlePause" style="margin-top: 5px">暂停视频编辑题目</el-button>
+        <el-button type="primary" size="small" style="margin-top: 5px" @click="handlePause">暂停视频编辑题目</el-button>
         <!-- <el-button @click="handleMultilingual">多语言</el-button> -->
         <ul v-if="data.file_info_list.length > 0" class="file-list">
           <li v-for="(file, i) in data.file_info_list" :key="i">
@@ -48,14 +48,12 @@
         v-if="exerciseFlag"
         :visible.sync="exerciseFlag"
         width="1400px"
-        append-to-body
-        :show-close="true"
         title="练习题"
         :close-on-click-modal="false"
-        class="module-content"
-        @close="handleCancle"
+        custom-class="module-content"
+        @close="handleCancale"
       >
-        <exercise-add :exercise-content="exerciseContent" @handleCancle="handleCancle" @submitAdd="submitAdd" />
+        <exercise-add :exercise-content="exerciseContent" @handleCancale="handleCancale" @submitAdd="submitAdd" />
       </el-dialog>
       <MultilingualFill
         :visible.sync="multilingualVisible"
@@ -114,26 +112,25 @@ export default {
     };
   },
   watch: {
-    'data.video_list': {
-      handler(val, oldVal) {
-        if (val !== oldVal) {
-        }
-      },
-      immediate: true,
-    },
     'data.property.file_list': 'handleMindMap',
   },
+  mounted() {},
+  beforeDestroy() {
+    if (this.player) {
+      this.player.dispose();
+    }
+  },
   methods: {
     // 初始化player
-    handleCreatPlayer(time) {
+    handleCreatePlayer() {
       let _this = this;
       let options = {
-        autoplay: false, //自动播放
+        autoplay: false, // 自动播放
         height: 500,
         width: 918,
-        controls: true, //用户可以与之交互的控件
-        loop: true, //视频一结束就重新开始
-        muted: false, //默认情况下将使所有音频静音
+        controls: true, // 用户可以与之交互的控件
+        loop: true, // 视频一结束就重新开始
+        muted: false, // 默认情况下将使所有音频静音
         playsinline: true,
         webkitPlaysinline: true,
         // aspectRatio:"16:9",//显示比率
@@ -154,8 +151,8 @@ export default {
       // 设置标点
       _this.handleMarkers();
       // 获取当前播放时间
-      this.player.on('timeupdate', function (event) {
-        _this.currentTime = this.currentTime().toFixed(3);
+      this.player.on('timeupdate', () => {
+        this.currentTime = this.player.currentTime().toFixed(3);
         // console.log(this.currentTime());
       });
     },
@@ -178,28 +175,7 @@ export default {
           'background-color': 'red',
           'border-radius': '50%',
         },
-        markers: markers,
-        // markers: [
-        //   {
-        //     time: 0.694313,
-        //     class: 'custom-marker-class',
-        //     text: '标记1',
-        //   },
-        //   {
-        //     time: 5.694313,
-        //     class: 'custom-marker-class',
-        //     text: '标记2',
-        //   },
-        //   {
-        //     time: 10.694313,
-        //     class: 'custom-marker-class',
-        //     text: '标记3',
-        //   },
-        //   {
-        //     time: 15.694313,
-        //     class: 'custom-marker-class',
-        //   },
-        // ],
+        markers,
       });
     },
     updateFileList({ file_list, file_id_list, file_info_list }) {
@@ -210,21 +186,12 @@ export default {
       if (file_list.length === 0) {
         this.data.file_info_list = [];
       } else {
-        let _this = this;
         this.$nextTick(() => {
-          _this.handleCreatPlayer();
+          this.handleCreatePlayer();
         });
         this.duration = file_list[0].media_duration;
       }
     },
-
-    handleData() {
-      // this.data.video_list.forEach((item) => {
-      //   GetFileURLMap({ file_id_list: [item.file_id] }).then(({ url_map }) => {
-      //     this.$set(item, 'file_url', url_map[item.file_id]);
-      //   });
-      // });
-    },
     handleMindMap() {
       // 思维导图数据
       let node_list = [];
@@ -249,7 +216,7 @@ export default {
         this.player.play();
       }
     },
-    handleCancle() {
+    handleCancale() {
       // this.sourceAddFlag = false;
       this.exerciseFlag = false;
       this.player.play();
@@ -325,16 +292,10 @@ export default {
           }
         })
         .catch(() => {
-          this.$emit('handleCancle');
+          this.$emit('handleCancale');
         });
     },
   },
-  mounted() {},
-  beforeDestroy() {
-    if (this.player) {
-      this.player.dispose();
-    }
-  },
 };
 </script>
 <style lang="scss" scoped>

+ 85 - 119
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -372,120 +372,113 @@ export default {
      * @returns {Object} 列的样式对象
      */
     computedColStyle(col) {
-      const grid = col.grid_list;
+      const grid = col.grid_list || [];
+      if (grid.length === 0) {
+        return {
+          width: col.width,
+          gridTemplateAreas: '',
+          gridTemplateColumns: '',
+          gridTemplateRows: '',
+        };
+      }
 
-      let maxCol = 0; // 最大列数
-      let rowList = new Map();
-      grid.forEach(({ row }) => {
-        rowList.set(row, (rowList.get(row) || 0) + 1);
+      // 先按 row 分组,避免后续重复 filter 扫描
+      const rowMap = new Map();
+      grid.forEach((item) => {
+        if (!rowMap.has(item.row)) {
+          rowMap.set(item.row, []);
+        }
+        rowMap.get(item.row).push(item);
       });
-      let curMaxRow = 0; // 当前数量最大 row 的值
-      rowList.forEach((value, key) => {
-        if (value > maxCol) {
-          maxCol = value;
-          curMaxRow = key;
+
+      let maxCol = 0;
+      let curMaxRow = 0;
+      rowMap.forEach((items, row) => {
+        if (items.length > maxCol) {
+          maxCol = items.length;
+          curMaxRow = row;
         }
       });
+
       // 计算 grid_template_areas
       let gridTemplateAreas = '';
-      let gridArr = [];
-      grid.forEach(({ grid_area, row }) => {
-        if (!gridArr[row - 1]) {
-          gridArr[row - 1] = [];
-        }
-        if (curMaxRow === row) {
-          gridArr[row - 1].push(`${grid_area}`);
+      rowMap.forEach((items, row) => {
+        let rowAreas = [];
+        if (row === curMaxRow) {
+          rowAreas = items.map((item) => item.grid_area);
         } else {
-          let filter = grid.filter((item) => item.row === row);
-          let find = filter.findIndex((item) => item.grid_area === grid_area);
-          let needNum = maxCol - filter.length; // 需要的数量
-
-          let str = '';
-          if (filter.length === 1) {
-            str = ` ${grid_area} `.repeat(needNum + 1);
+          const needNum = maxCol - items.length;
+          if (items.length === 1) {
+            rowAreas = Array(needNum + 1).fill(items[0].grid_area);
           } else {
-            let arr = this.splitInteger(needNum, filter.length);
-            str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
+            const splitArr = this.splitInteger(needNum, items.length);
+            rowAreas = items.flatMap((item, index) => Array(splitArr[index] + 1).fill(item.grid_area));
           }
-          gridArr[row - 1].push(`${str}`);
         }
-      });
-      gridArr.forEach((item) => {
-        gridTemplateAreas += `'${item.join(' ')}' `;
+        gridTemplateAreas += `'${rowAreas.join(' ')}' `;
       });
 
       // 计算 grid_template_columns
-      let gridTemplateColumns = '';
-      let max = { row: 0, num: 0 };
-      grid.forEach(({ row }) => {
-        // 计算出 row 的哪个值最多
-        let len = grid.filter((item) => item.row === row).length;
-        if (max.num < len) {
-          max.num = len;
-          max.row = row;
-        }
-      });
-      grid.forEach((item) => {
-        if (item.row === max.row) {
-          gridTemplateColumns += `${item.width} `;
-        }
-      });
+      const maxRowItems = rowMap.get(curMaxRow) || [];
+      const gridTemplateColumns = maxRowItems.length ? `${maxRowItems.map((item) => item.width).join(' ')} ` : '';
 
       // 计算 grid_template_rows
-      let gridTemplateRows = '';
-      // 将 grid 按照 row 分组
-      let gridMap = new Map();
-      grid.forEach((item) => {
-        if (!gridMap.has(item.row)) {
-          gridMap.set(item.row, []);
+      const previewById = new Map();
+      (this.$refs.preview || []).forEach((child) => {
+        const id = child?.$el?.dataset?.id;
+        if (id) {
+          previewById.set(id, child);
         }
-        gridMap.get(item.row).push({ height: item.height, id: item.id });
       });
+      const hasOperationById = (id) => {
+        const component = previewById.get(id);
+        return Boolean(component && component.$el.querySelector('.operation'));
+      };
+      const toNumberHeight = (height) => {
+        const num = Number(String(height).replace('px', ''));
+        return Number.isFinite(num) ? num : NaN;
+      };
 
-      gridMap.forEach((value) => {
-        if (value.length === 1) {
-          const component = this.$refs.preview?.find(
-            (child) => child.$el && child.$el.dataset && child.$el.dataset.id === value[0].id,
-          );
+      let gridTemplateRows = '';
+      rowMap.forEach((items) => {
+        if (items.length === 1) {
+          const current = items[0];
+          if (current.height === 'auto') {
+            gridTemplateRows += 'auto ';
+            return;
+          }
+          let baseHeight = toNumberHeight(current.height);
+          if (Number.isNaN(baseHeight)) {
+            gridTemplateRows += `${current.height} `;
+            return;
+          }
+          if (hasOperationById(current.id)) {
+            baseHeight += 48;
+          }
+          gridTemplateRows += `${baseHeight}px `;
+          return;
+        }
 
-          const hasOperation = component && component.$el.querySelector('.operation') !== null; // 判断是否有操作按钮
-          let height = hasOperation ? `${Number(value[0].height.replace('px', '')) + 48}px` : value[0].height;
+        const nonAutoItems = items.filter((item) => item.height !== 'auto');
+        if (nonAutoItems.length === 0) {
+          gridTemplateRows += 'auto ';
+          return;
+        }
 
-          gridTemplateRows += `${height} `;
-        } else {
-          let isAllAuto = value.every((item) => item.height === 'auto'); // 是否全是 auto
-          if (!isAllAuto) {
-            // 判断 value 中height 最大的组件是那个,不包括 auto 的组件
-            let maxHeight = 0;
-            let maxId = '';
-            value.forEach((item) => {
-              if (item.height !== 'auto') {
-                const heightNum = Number(item.height.replace('px', ''));
-                if (heightNum > maxHeight) {
-                  maxHeight = heightNum;
-                  maxId = item.id;
-                }
-              }
-            });
-            // 判断最高组件是否有操作按钮
-            const component = this.$refs.preview?.find(
-              (child) => child.$el && child.$el.dataset && child.$el.dataset.id === maxId,
-            );
-            const hasOperation = component && component.$el.querySelector('.operation') !== null; // 判断是否有操作按钮
-            if (hasOperation) {
-              maxHeight += 48;
-            }
-            // 将 maxId 的 height 设置为 maxHeight
-            value.forEach((item) => {
-              if (item.id === maxId) {
-                item.height = `${maxHeight}px`;
-              }
-            });
+        let maxItem = null;
+        let maxHeight = 0;
+        nonAutoItems.forEach((item) => {
+          const current = toNumberHeight(item.height);
+          if (!Number.isNaN(current) && current > maxHeight) {
+            maxHeight = current;
+            maxItem = item;
           }
-          gridTemplateRows += isAllAuto
-            ? 'auto '
-            : `${Math.max(...value.map((item) => Number(item.height.replace('px', ''))))}px `;
+        });
+
+        if (maxItem && hasOperationById(maxItem.id)) {
+          maxHeight += 48;
         }
+        gridTemplateRows += `${maxHeight}px `;
       });
 
       return {
@@ -507,30 +500,6 @@ export default {
         background: back,
       } = this.background || {};
 
-      const hasNoRows = !Array.isArray(this.data?.row_list) || this.data.row_list.length === 0; // 判断是否没有行
-      const projectCover = this.project?.cover_image_file_url || '';
-
-      // 优先在空行时使用背景图或项目封面
-      if (hasNoRows) {
-        const backgroundImage = hasNoRows ? bcImgUrl || projectCover : bcImgUrl;
-
-        // 保护性读取位置/大小值,避免 undefined 导致字符串 "undefined%"
-        const widthPct = typeof pos.width === 'undefined' ? '' : pos.width;
-        const heightPct = typeof pos.height === 'undefined' ? '' : pos.height;
-        const leftPct = typeof pos.left === 'undefined' ? '' : pos.left;
-        const topPct = typeof pos.top === 'undefined' ? '' : pos.top;
-
-        const hasBcImg = Boolean(bcImgUrl);
-        const backgroundSize = hasBcImg ? `${widthPct}% ${heightPct}%` : hasNoRows ? 'contain' : '';
-        const backgroundPosition = hasBcImg ? `${leftPct}% ${topPct}%` : hasNoRows ? 'center' : '';
-
-        return {
-          backgroundImage: backgroundImage ? `url(${backgroundImage})` : '',
-          backgroundSize,
-          backgroundPosition,
-        };
-      }
-
       let canvasStyle = {
         backgroundSize: bcImgUrl ? `${pos.width}% ${pos.height}%` : '',
         backgroundPosition: bcImgUrl ? `${pos.left}% ${pos.top}%` : '',
@@ -961,9 +930,6 @@ export default {
       if (!_offset) _offset = 0;
       const element = document.querySelector(`div[data-id="${dataId}"]`);
       if (element) {
-        const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
-        const offsetPosition = elementPosition - _offset;
-
         element.scrollIntoView({
           behavior: 'smooth', // 滚动行为:'auto' | 'smooth'
           block: 'center', // 垂直对齐:'start' | 'center' | 'end' | 'nearest'

+ 6 - 0
src/views/book/courseware/preview/common/AnswerAnalysis.vue

@@ -6,6 +6,7 @@
     :visible="visible"
     width="65vw"
     :close-on-click-modal="false"
+    :modal="modal"
     :before-close="handleClose"
   >
     <!-- 正确答案 -->
@@ -104,6 +105,11 @@ import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPl
 export default {
   name: 'AnswerAnalysis',
   components: { AudioPlay, VideoPlay },
+  inject: {
+    modal: {
+      default: true,
+    },
+  },
   props: {
     visible: {
       type: Boolean,

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

@@ -18,6 +18,7 @@
                 :key="`${i}-${j}`"
                 class="content"
                 :paragraph-list="li.paragraph_list"
+                :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />
@@ -119,6 +120,7 @@
                 :key="`${i}-${j}`"
                 class="content"
                 :paragraph-list="li.paragraph_list"
+                :rich-text-list="li.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />

+ 4 - 2
src/views/book/courseware/preview/components/judge/JudgePreview.vue

@@ -5,7 +5,7 @@
     <div class="main">
       <ul class="option-list">
         <li
-          v-for="({ content, mark, paragraph_list }, i) in data.option_list"
+          v-for="({ content, mark, paragraph_list, rich_text_list }, i) in data.option_list"
           :key="mark"
           :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
           :class="['option-item', { active: isAnswer(mark) }]"
@@ -19,6 +19,7 @@
               v-if="isEnable(data.property.view_pinyin)"
               class="content"
               :paragraph-list="paragraph_list"
+              :rich-text-list="rich_text_list"
               :pinyin-position="data.property.pinyin_position"
               :is-preview="true"
             />
@@ -70,7 +71,7 @@
     >
       <ul slot="right-answer" class="option-list">
         <li
-          v-for="({ content, mark, paragraph_list }, i) in data.option_list"
+          v-for="({ content, mark, paragraph_list, rich_text_list }, i) in data.option_list"
           :key="mark"
           :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
           :class="['option-item', { active: isAnswer(mark) }]"
@@ -84,6 +85,7 @@
               v-if="isEnable(data.property.view_pinyin)"
               class="content"
               :paragraph-list="paragraph_list"
+              :rich-text-list="rich_text_list"
               :pinyin-position="data.property.pinyin_position"
               :is-preview="true"
             />

+ 2 - 1
src/views/book/courseware/preview/components/matching/MatchingPreview.vue

@@ -6,7 +6,7 @@
     <div class="main">
       <ul ref="list" class="option-list">
         <li v-for="(item, i) in data.option_list" :key="i" class="list-item">
-          <template v-for="({ content, mark, multilingual, paragraph_list }, j) in item">
+          <template v-for="({ content, mark, multilingual, paragraph_list, rich_text_list }, j) in item">
             <div
               v-if="content"
               :key="mark"
@@ -25,6 +25,7 @@
                 v-if="isEnable(data.property.view_pinyin)"
                 class="content"
                 :paragraph-list="paragraph_list"
+                :rich-text-list="rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />

+ 5 - 10
src/views/book/courseware/preview/components/record_input/SoundRecord.vue

@@ -52,14 +52,9 @@
         v-if="microphoneStatus"
         icon-class="luyin-ing"
         size="24"
-        :style="{ color: attrib.topic_color ? attrib.topic_color : '#DCDFE6' }"
-      />
-      <SvgIcon
-        v-else
-        icon-class="luyin-radio-button"
-        size="24"
-        :style="{ color: attrib.topic_color ? attrib.topic_color : '#DCDFE6' }"
+        :style="{ color: attrib?.topic_color ?? '#DCDFE6' }"
       />
+      <SvgIcon v-else icon-class="luyin-radio-button" size="24" :style="{ color: attrib?.topic_color ?? '#DCDFE6' }" />
     </div>
 
     <span
@@ -71,7 +66,7 @@
       ]"
       :style="{
         fontSize: attrib && attrib.font_size ? attrib.font_size : '',
-        color: microphoneStatus && attrib && attrib.topic_color ? attrib.topic_color : '#DCDFE6',
+        color: microphoneStatus && attrib && attrib?.topic_color ? attrib?.topic_color : '#DCDFE6',
       }"
       >{{ isPlaying ? '-' : '' }}{{ handleDateTime(recordtime) }}</span
     >
@@ -92,7 +87,7 @@
       <SvgIcon
         icon-class="luyin-delete"
         size="24"
-        :style="{ color: hasMicro ? (attrib.topic_color ? attrib.topic_color : '#DCDFE6') : 'rgba(0, 0, 0, 0.3)' }"
+        :style="{ color: hasMicro ? (attrib?.topic_color ?? '#DCDFE6') : 'rgba(0, 0, 0, 0.3)' }"
       />
     </a>
   </div>
@@ -102,6 +97,7 @@
 import Recorder from 'js-audio-recorder'; // 录音插件
 
 export default {
+  inject: ['convertText'],
   props: {
     wavData: {
       type: Object,
@@ -145,7 +141,6 @@ export default {
       default: () => {},
     },
   },
-  inject: ['convertText'],
   data() {
     return {
       recorder: new Recorder({

+ 4 - 2
src/views/book/courseware/preview/components/select/SelectPreview.vue

@@ -9,7 +9,7 @@
         :style="{ flexDirection: data.property.arrange_type === arrangeTypeList[0].value ? 'row' : 'column' }"
       >
         <li
-          v-for="({ content, mark, multilingual, paragraph_list }, i) in data.option_list"
+          v-for="({ content, mark, multilingual, paragraph_list, rich_text_list }, i) in data.option_list"
           :key="mark"
           :style="{ cursor: disabled ? 'not-allowed' : 'pointer', borderColor: data.unified_attrib?.topic_color }"
           :class="['option-item', { active: isAnswer(mark) }, ...computedAnswerClass(mark)]"
@@ -23,6 +23,7 @@
             v-if="isEnable(data.property.view_pinyin)"
             class="content"
             :paragraph-list="paragraph_list"
+            :rich-text-list="rich_text_list"
             :pinyin-position="data.property.pinyin_position"
             :is-preview="true"
           />
@@ -58,7 +59,7 @@
         :style="{ flexDirection: data.property.arrange_type === arrangeTypeList[0].value ? 'row' : 'column' }"
       >
         <li
-          v-for="({ content, mark, multilingual, paragraph_list }, i) in data.option_list"
+          v-for="({ content, mark, multilingual, paragraph_list, rich_text_list }, i) in data.option_list"
           :key="mark"
           :style="{ cursor: disabled ? 'not-allowed' : 'pointer', borderColor: data.unified_attrib?.topic_color }"
           :class="['option-item', ...computedAnswerClass(mark, 'answer-analysis')]"
@@ -72,6 +73,7 @@
             v-if="isEnable(data.property.view_pinyin)"
             class="content"
             :paragraph-list="paragraph_list"
+            :rich-text-list="rich_text_list"
             :pinyin-position="data.property.pinyin_position"
             :is-preview="true"
           />

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

@@ -28,6 +28,7 @@
                 v-if="isEnable(data.property.view_pinyin)"
                 class="content"
                 :paragraph-list="item.paragraph_list"
+                :rich-text-list="item.rich_text_list"
                 :pinyin-position="data.property.pinyin_position"
                 :is-preview="true"
               />

+ 1 - 0
src/views/book/courseware/preview/components/video_interaction/ExercisePreview.vue

@@ -29,6 +29,7 @@ export default {
   provide() {
     return {
       getPermissionControl: () => this.permissionControl,
+      modal: false,
     };
   },
   props: ['exercise_id', 'feed_back'],

+ 16 - 16
src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue

@@ -13,8 +13,8 @@
         @timeupdate="handleTimeUpdate"
       ></video> -->
       <video
-        ref="videoPlayer"
         id="interaction-preview-video"
+        ref="videoPlayer"
         :src="data.video_list[0].file_url"
         width="100%"
         height="400"
@@ -24,6 +24,7 @@
     </div>
     <!-- v-if="Object.keys(this.userAnswer).length === data.file_info_list.length" -->
     <el-button
+      v-if="data.property.feed_back === 'total'"
       type="primary"
       :style="{
         background:
@@ -33,7 +34,6 @@
         marginTop: '5px',
       }"
       @click="lookReport"
-      v-if="data.property.feed_back === 'total'"
       >{{ convertText('查看答题报告') }}</el-button
     >
     <AnswerCorrect
@@ -59,9 +59,9 @@
     >
       <ExercisePreview
         :exercise_id="exercise_id"
+        :feed_back="data.property.feed_back"
         @handleCancle="handleClose"
         @submitAdd="submitAdd"
-        :feed_back="data.property.feed_back"
       />
       <!-- <iframe v-if="visible" :src="newpath" width="100%" :height="iframeHeight" frameborder="0"></iframe> -->
     </el-dialog>
@@ -140,6 +140,11 @@ export default {
     this.initData();
   },
   mounted() {},
+  beforeDestroy() {
+    if (this.player) {
+      this.player.dispose();
+    }
+  },
   methods: {
     initData() {
       this.data.file_info_list = this.data.file_info_list.sort((a, b) => Number(a.currentTime) - Number(b.currentTime));
@@ -157,12 +162,12 @@ export default {
     handleCreatPlayer() {
       let _this = this;
       let options = {
-        autoplay: false, //自动播放
+        autoplay: false, // 自动播放
         height: 500,
         width: 1000,
-        controls: true, //用户可以与之交互的控件
-        loop: true, //视频一结束就重新开始
-        muted: false, //默认情况下将使所有音频静音
+        controls: true, // 用户可以与之交互的控件
+        loop: true, // 视频一结束就重新开始
+        muted: false, // 默认情况下将使所有音频静音
         playsinline: true,
         webkitPlaysinline: true,
         // aspectRatio:"16:9",//显示比率
@@ -183,8 +188,8 @@ export default {
       // 设置标点
       _this.handleMarkers();
       // 获取当前播放时间
-      this.player.on('timeupdate', function (event) {
-        _this.handleTimeUpdate(this.currentTime());
+      this.player.on('timeupdate', () => {
+        this.handleTimeUpdate(this.player.currentTime());
         // console.log(this.currentTime());
       });
     },
@@ -207,7 +212,7 @@ export default {
           'background-color': 'red',
           'border-radius': '50%',
         },
-        markers: markers,
+        markers,
       });
     },
     handleTimeUpdate(currentTime) {
@@ -255,7 +260,7 @@ export default {
       let total = this.data.file_info_list.length;
       Object.keys(this.userAnswer).forEach((key) => {
         if (this.userAnswer[key].is_right) {
-          num++;
+          num += 1;
         }
       });
       this.reportResult = {
@@ -267,11 +272,6 @@ export default {
       this.reportFlag = true;
     },
   },
-  beforeDestroy() {
-    if (this.player) {
-      this.player.dispose();
-    }
-  },
 };
 </script>
 

+ 17 - 1
src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue

@@ -192,7 +192,6 @@
           @handleWav="handleWav"
         />
         <AudioCompare
-          :style="{ flex: 1 }"
           :theme-color="themeColor"
           :wavblob="wavblob"
           type="full"
@@ -893,6 +892,23 @@ $border-color: #e6e6e6;
       :deep .record {
         max-width: 280px;
       }
+
+      :deep .compara-content {
+        margin-left: 8px;
+      }
+
+      :deep .compare-box,
+      :deep .compare-disable {
+        width: 20px;
+        height: 20px;
+      }
+
+      :deep .play-icon-big,
+      :deep .content-voices {
+        width: 20px;
+        height: 20px;
+        margin-top: 2px;
+      }
     }
   }
 }

+ 3 - 3
src/views/book/courseware/preview/components/voice_matrix/components/AudioCompareMatrix.vue

@@ -34,11 +34,11 @@
     <template v-else>
       <img
         :src="
-          type == 'full'
+          type === 'full'
             ? require('@/assets/voice_matrix/compare-disable-24.png')
             : require('@/assets/voice_matrix/compare-disable.png')
         "
-        :class="['compare-disable', type == 'full' ? 'compare-disable-big' : '']"
+        :class="['compare-disable', type === 'full' ? 'compare-disable-big' : '']"
       />
     </template>
   </div>
@@ -73,7 +73,7 @@ export default {
     };
   },
   watch: {
-    matrixSelectLrc(newVal) {
+    matrixSelectLrc() {
       if (this.unWatch) {
         this.unWatch();
         this.unWatch = null;

+ 2 - 1
src/views/personal_workbench/project/components/UploadFile.vue

@@ -18,7 +18,7 @@
         >
           <el-button>选取{{ labelText }}文件</el-button>
         </el-upload>
-        <el-button size="small" type="primary" @click="uploadFiles">上传</el-button>
+        <el-button size="small" type="primary" @click="uploadFiles">本地上传</el-button>
       </div>
     </div>
     <el-divider />
@@ -161,6 +161,7 @@ export default {
         return;
       }
       this.content.file_list = [...this.content.file_list, ...files];
+      this.uploadFiles();
     },
 
     handleExceed(files, fileList) {