Forráskód Böngészése

完成预览前期准备

dusenyao 2 éve
szülő
commit
113932f395
22 módosított fájl, 472 hozzáadás és 205 törlés
  1. 4 0
      src/icons/svg/course/course-close.svg
  2. 4 0
      src/icons/svg/play.svg
  3. 4 0
      src/icons/svg/record-stop.svg
  4. 4 0
      src/icons/svg/recording.svg
  5. 4 0
      src/styles/common.scss
  6. 50 5
      src/views/teacher/create_course/step_table/create_task/components/data/TaskClassify.js
  7. 1 2
      src/views/teacher/create_course/step_table/create_task/components/data/TaskData.js
  8. 28 51
      src/views/teacher/create_course/step_table/create_task/components/data/TaskType.js
  9. 14 14
      src/views/teacher/create_course/step_table/create_task/components/layouts/TaskEditor.vue
  10. 3 3
      src/views/teacher/create_course/step_table/create_task/components/pop-up/SelectTaskClassify.vue
  11. 54 11
      src/views/teacher/create_course/step_table/create_task/components/pop-up/VideoRecording.vue
  12. 15 0
      src/views/teacher/create_course/step_table/create_task/components/previews/index.vue
  13. 1 3
      src/views/teacher/create_course/step_table/create_task/components/task_template/TaskTemplate.vue
  14. 21 80
      src/views/teacher/create_course/step_table/create_task/components/task_template/components/AudioShow.vue
  15. 39 10
      src/views/teacher/create_course/step_table/create_task/components/task_template/components/TemplateRecording.vue
  16. 90 0
      src/views/teacher/create_course/step_table/create_task/components/task_template/components/VideoShow.vue
  17. 101 1
      src/views/teacher/create_course/step_table/create_task/components/task_template/components/play.js
  18. 4 2
      src/views/teacher/create_course/step_table/create_task/components/task_template/components/recording.js
  19. 1 2
      src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/SubTask.vue
  20. 1 3
      src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/SubtaskItem.vue
  21. 1 3
      src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/data.js
  22. 28 15
      src/views/teacher/create_course/step_table/create_task/index.vue

+ 4 - 0
src/icons/svg/course/course-close.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.66602 2.66699L13.3327 13.3337" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66602 13.3337L13.3327 2.66699" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
src/icons/svg/play.svg

@@ -0,0 +1,4 @@
+<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 26L18.5 22V14L12 10V26ZM18.5 22L25 18L18.5 14V22Z" fill="white"/>
+<rect x="0.5" y="0.5" width="35" height="35" rx="17.5" stroke="white"/>
+</svg>

+ 4 - 0
src/icons/svg/record-stop.svg

@@ -0,0 +1,4 @@
+<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="20" cy="20" r="19" stroke="#D83434" stroke-width="2"/>
+<rect x="12" y="12" width="16" height="16" rx="4" fill="#D83434"/>
+</svg>

+ 4 - 0
src/icons/svg/recording.svg

@@ -0,0 +1,4 @@
+<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="20" cy="20" r="19" stroke="#D83434" stroke-width="2"/>
+<circle cx="20" cy="20" r="12" fill="#D83434"/>
+</svg>

+ 4 - 0
src/styles/common.scss

@@ -21,3 +21,7 @@ svg.svg-normal.svg-icon {
   width: 24px;
   height: 24px;
 }
+
+.display-none {
+  display: none;
+}

+ 50 - 5
src/views/teacher/create_course/step_table/create_task/components/data/TaskClassify.js

@@ -1,3 +1,6 @@
+import { ref, provide } from 'vue';
+import { addTaskList } from './TaskData';
+
 // 任务教学类型
 export const MORE_NAME = 'more';
 
@@ -39,11 +42,53 @@ export const taskClassify = [
   }
 ];
 
-export function useTaskClassify() {
-  function getTaskClassifyAttr(type, propName = 'name') {
-    if (!type) return '';
-    return taskClassify.find((item) => item.type === type)[propName];
+/**
+ * 获取教学类型某个属性对应值
+ * @param {String} type
+ * @param {String} propName
+ * @returns {String}
+ */
+export function getTaskClassifyAttr(type, propName = 'name') {
+  if (!type) return '';
+  return taskClassify.find((item) => item.type === type)[propName];
+}
+
+/**
+ * 选择任务分类
+ * @param {Object} curTaskTypeObj
+ */
+export function useSelectTaskClassify(curTaskTypeObj) {
+  let visible = ref(false); // 控制显隐
+  let isAddSubtask = ref(false); // 是否添加子任务
+  let selectFn = null;
+  function changeVisible(show, attr) {
+    visible.value = show;
+    let { isAdd, fn } = attr;
+    isAddSubtask.value = isAdd;
+    selectFn = fn;
+  }
+  provide('visible', {
+    visible,
+    changeVisible
+  });
+
+  // 添加任务
+  function addTask(taskClassifyType) {
+    addTaskList(curTaskTypeObj.value.sidebarListName, taskClassifyType);
+  }
+
+  function selectTaskClassify(taskClassifyType) {
+    if (!taskClassifyType) return;
+    visible.value = false;
+    selectFn ? selectFn(taskClassifyType) : addTask(taskClassifyType);
+    if (selectFn) selectFn = null;
   }
 
-  return { getTaskClassifyAttr };
+  provide('addTask', addTask);
+
+  return {
+    visible,
+    isAddSubtask,
+    selectTaskClassify
+  };
 }

+ 1 - 2
src/views/teacher/create_course/step_table/create_task/components/data/TaskData.js

@@ -84,8 +84,7 @@ export const messageItem = [
   },
   {
     message_type: 'video',
-    file_id: '',
-    _file_url: ''
+    file_id: ''
   }
 ];
 

+ 28 - 51
src/views/teacher/create_course/step_table/create_task/components/data/TaskType.js

@@ -1,8 +1,9 @@
 import { computed, ref, provide } from 'vue';
 import { useScale } from '../util/wheel';
+
 import TaskExplain from '../TaskExplain/index.vue';
 import TaskTemplate from '../task_template/index.vue';
-import { addTaskList } from './TaskData';
+import PreviewPage from '../previews';
 
 /**
  * 任务类型相关
@@ -49,75 +50,51 @@ export const taskTypeArray = [
     sidebarListName: 'after_task_list'
   }
 ];
+// 页面状态
+export const pageStateList = ['editor', 'preview'];
+export let curPageState = ref(pageStateList[0]); // 当前页面状态
+export function changeCurPageState(type) {
+  curPageState.value = type;
+}
+export let isMindMapping = ref(false);
 
 export function useTaskType() {
-  const mainPageTypeList = [TaskExplain, TaskTemplate]; // 主页面类型列表
-  let curPageType = ref(mainPageTypeList[1]); // 当前页面类型
-  let curSelectTaskType = ref(taskTypeArray[1].type); // 当前选中的任务类型
+  const mainPageTypeList = [TaskExplain, TaskTemplate, PreviewPage]; // 主页面类型列表
 
-  let curTaskType = computed(() => {
-    if (curPageType.value === mainPageTypeList[0]) return TASK_EXPLAIN;
-    return curSelectTaskType.value;
-  }); // 当前任务类型
+  let curTaskType = ref(taskTypeArray[1].type); // 当前任务类型
+
+  let curPageType = computed(() => {
+    if (curPageState.value === pageStateList[1]) return mainPageTypeList[2];
+    if (curTaskType.value === TASK_EXPLAIN) return mainPageTypeList[0];
+    return mainPageTypeList[1];
+  }); // 当前页面类型
 
   // 当前任务类型对象
   let curTaskTypeObj = computed(() => taskTypeArray.find(({ type }) => type === curTaskType.value));
   provide('curTaskTypeObj', curTaskTypeObj);
 
-  // 切换当前页面类型
-  function changeCurPageType(type) {
-    curPageType.value = type;
-  }
-
   const { resetScale } = useScale();
-  // 切换任务类型
+
+  // 处理切换任务类型
   function changeTaskType(type) {
     if (type === TASK_EXPLAIN) {
-      changeCurPageType(mainPageTypeList[0]);
-      resetScale();
-    } else {
-      curSelectTaskType.value = type;
-      changeCurPageType(mainPageTypeList[1]);
+      curTaskType.value = type;
+      return resetScale();
+    }
+    if ([taskTypeArray[1].type, taskTypeArray[2].type, taskTypeArray[3].type].includes(type)) {
+      return (curTaskType.value = type);
     }
-  }
-
-  // 显示选择任务类型
-  let visible = ref(false);
-  let isAddSubtask = ref(false); // 是否添加子任务
-  let selectFn = null;
-  function changeVisible(show, attr) {
-    visible.value = show;
-    let { isAdd, fn } = attr;
-    isAddSubtask.value = isAdd;
-    selectFn = fn;
-  }
-  provide('visible', {
-    visible,
-    changeVisible
-  });
-
-  // 添加任务
-  function addTask(taskClassifyType) {
-    addTaskList(curTaskTypeObj.value.sidebarListName, taskClassifyType);
-  }
 
-  function selectTaskClassify(taskClassifyType) {
-    if (!taskClassifyType) return;
-    visible.value = false;
-    selectFn ? selectFn(taskClassifyType) : addTask(taskClassifyType);
-    if (selectFn) selectFn = null;
+    if (type === pageStateList[1]) {
+      return changeCurPageState(pageStateList[1]);
+    }
   }
 
-  provide('addTask', addTask);
-
   return {
     curTaskType,
     curTaskTypeObj,
     curPageType,
     mainPageTypeList,
-    visible,
-    isAddSubtask,
-    changeTaskType,
-    selectTaskClassify
+    changeTaskType
   };
 }

+ 14 - 14
src/views/teacher/create_course/step_table/create_task/components/layouts/TaskEditor.vue

@@ -16,15 +16,19 @@
       </div>
     </div>
 
-    <RightSidebar v-show="curPageType !== mainPageTypeList[0]" />
+    <RightSidebar v-show="curPageType === mainPageTypeList[1]" />
 
-    <SelectTaskClassify :visible="visible" :is-add-subtask="isAddSubtask" @select-task-classify="selectTaskClassify" />
+    <SelectTaskClassify
+      :visible.sync="visible"
+      :is-add-subtask="isAddSubtask"
+      @select-task-classify="selectTaskClassify"
+    />
     <SelectCourse
       :dialog-visible="visibleSelectCourse"
       @selectCourse="selectCourseList"
       @dialogClose="visibleSelectCourse = false"
     />
-    <VideoRecording :visible="visible_video" />
+    <VideoRecording :visible.sync="visible_video" @sendVideo="sendVideo" />
   </div>
 </template>
 
@@ -37,6 +41,7 @@ export default {
 <script setup>
 import { computed, provide, ref } from 'vue';
 import { useTaskType } from '../data/TaskType.js';
+import { useSelectTaskClassify } from '../data/TaskClassify';
 import { useMouseEvent } from '../util/mouseEvent.js';
 import { useScale, useWheel, scale } from '../util/wheel.js';
 import { taskData } from '../data/TaskData';
@@ -52,16 +57,11 @@ import VideoRecording from '../pop-up/VideoRecording.vue';
 
 const center = ref(); // vue3 获取 refs 写法,需要同名,且传空
 
-const {
-  visible,
-  isAddSubtask,
-  curTaskType,
-  curTaskTypeObj,
-  curPageType,
-  mainPageTypeList,
-  changeTaskType,
-  selectTaskClassify
-} = useTaskType();
+const { curTaskType, curTaskTypeObj, curPageType, mainPageTypeList, changeTaskType } = useTaskType();
+
+defineExpose({ changeTaskType }); // 显式指定在 <script setup> 组件中要暴露出去的属性
+
+const { visible, isAddSubtask, selectTaskClassify } = useSelectTaskClassify(curTaskTypeObj);
 
 const { centerStyle } = useMouseEvent(center);
 
@@ -77,7 +77,7 @@ let curTaskTemplateList = computed(() => {
 provide('curTaskTemplateList', curTaskTemplateList);
 
 // 录像
-const { visible_video } = videoRecording();
+const { visible_video, sendVideo } = videoRecording();
 
 useOtherPausePlay();
 

+ 3 - 3
src/views/teacher/create_course/step_table/create_task/components/pop-up/SelectTaskClassify.vue

@@ -6,7 +6,7 @@
     width="610px"
     :before-close="
       () => {
-        emit('update:visible', false);
+        emits('update:visible', false);
       }
     "
     :modal="false"
@@ -39,7 +39,7 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['select-task-classify', 'update:visible']);
+const emits = defineEmits(['select-task-classify', 'update:visible']);
 
 function getSubtaskIsShow(type) {
   if (MORE_NAME === type) return false;
@@ -48,7 +48,7 @@ function getSubtaskIsShow(type) {
 }
 
 let selectTaskClassify = funcLock((type) => {
-  emit('select-task-classify', type);
+  emits('select-task-classify', type);
 }, 500);
 </script>
 

+ 54 - 11
src/views/teacher/create_course/step_table/create_task/components/pop-up/VideoRecording.vue

@@ -1,18 +1,28 @@
 <template>
   <el-dialog
     :append-to-body="true"
-    top="40vh"
+    top="20vh"
     :visible="visible"
     width="1122px"
     :before-close="
       () => {
-        emit('update:visible', false);
+        emits('update:visible', false);
       }
     "
     :modal="false"
   >
-    <video ref="video"></video>
-    <div></div>
+    <video ref="video" class="video" autoplay></video>
+    <div class="footer">
+      <svg-icon
+        :icon-class="isRecording ? 'record-stop' : 'recording'"
+        class-name="record"
+        @click="() => (isRecording ? closeVideo() : startVideo())"
+      />
+      <div>
+        <el-button @click="emits('update:visible', false)">取消</el-button>
+        <el-button type="primary" @click="sendVideo">发送</el-button>
+      </div>
+    </div>
   </el-dialog>
 </template>
 
@@ -21,6 +31,8 @@ import { ref, watch } from 'vue';
 import { funcLock } from '@/utils/common';
 import { videoRecording } from '../task_template/components/recording.js';
 import { fileUploadPrimordial } from '@/api/app.js';
+import { messageItem } from '../data/TaskData';
+import { Message } from 'element-ui';
 
 defineProps({
   visible: {
@@ -28,23 +40,54 @@ defineProps({
     required: true
   }
 });
-const emit = defineEmits(['sendVideo', 'update:visible']);
+const emits = defineEmits(['sendVideo', 'update:visible']);
 
 let video = ref();
 let { blob, isRecording, startVideo, closeVideo } = videoRecording(video);
 
+let data = null;
 watch(blob, (newVal) => {
   let formData = new FormData();
-  formData.append('录像1.mp4', newVal, '录像1.mp4');
-  fileUploadPrimordial('Mid', formData).then((data) => {
-    console.log(data);
-    // message_list.value.push(Object.assign({}, messageItem[2], { file_id }));
+  formData.append('录像.mp4', newVal, '录像.mp4');
+  fileUploadPrimordial('Mid', formData).then(({ file_info_list }) => {
+    if (file_info_list.length > 0) {
+      data = Object.assign({}, messageItem[2], {
+        file_id: file_info_list[0].file_id
+      });
+    }
   });
 });
 
 let sendVideo = funcLock(() => {
-  emit('sendVideo');
+  if (isRecording.value) return Message.warning('视频录制中...');
+  if (!data) return Message.warning('未录制视频');
+  emits('sendVideo', data);
 }, 500);
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.el-dialog {
+  .video {
+    width: 100%;
+    height: 470px;
+    background-color: #010101;
+  }
+
+  .footer {
+    display: flex;
+    justify-content: space-between;
+    margin-top: 24px;
+
+    .record {
+      width: 40px;
+      height: 40px;
+      cursor: pointer;
+    }
+
+    .el-button--primary {
+      background-color: #2a76e8;
+      border-color: #2a76e8;
+    }
+  }
+}
+</style>

+ 15 - 0
src/views/teacher/create_course/step_table/create_task/components/previews/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div>
+    <!-- <keep-alive>
+      <component :is="componentId" />
+    </keep-alive> -->
+  </div>
+</template>
+
+<script setup>
+import { inject, ref, onActivated, onDeactivated, provide } from 'vue';
+import { isMindMapping } from '../data/TaskType';
+
+const curTaskTemplateList = inject('curTaskTemplateList');
+const curTaskTypeObj = inject('curTaskTypeObj');
+</script>

+ 1 - 3
src/views/teacher/create_course/step_table/create_task/components/task_template/TaskTemplate.vue

@@ -87,7 +87,7 @@ export default {
 
 <script setup>
 import { ref, inject, computed, provide } from 'vue';
-import { useTaskClassify, taskClassify } from '../data/TaskClassify';
+import { getTaskClassifyAttr, taskClassify } from '../data/TaskClassify';
 import { useSubtask } from './subtask/data';
 import { taskData } from '../data/TaskData';
 
@@ -115,8 +115,6 @@ const isActivated = inject('isActivated');
 
 const { changeVisible } = inject('visible');
 
-const { getTaskClassifyAttr } = useTaskClassify();
-
 let curTaskObject = computed(() => {
   return taskData.value[props.listName][props.index];
 });

+ 21 - 80
src/views/teacher/create_course/step_table/create_task/components/task_template/components/AudioShow.vue

@@ -4,10 +4,10 @@
       <svg-icon
         icon-class="common-playing"
         class-name="common-playing"
-        @click="() => (audio.playing ? pause() : play())"
+        @click="() => (state.playing ? pause() : play())"
       />
       <span class="time">
-        {{ audio.maxTime ? secondFormatConversion(audio.maxTime - audio.currentTime) : '' }}
+        {{ state.maxTime ? secondFormatConversion(state.maxTime - state.currentTime) : '' }}
       </span>
     </div>
     <audio
@@ -32,9 +32,9 @@ export default {
 </script>
 
 <script setup>
-import { inject, ref, watch } from 'vue';
-import { GetFileStoreInfo } from '@/api/app.js';
+import { ref, watch } from 'vue';
 import { secondFormatConversion } from '@/utils/index';
+import { usePlay } from './play';
 
 const props = defineProps({
   fileId: {
@@ -47,93 +47,34 @@ const props = defineProps({
   }
 });
 
-let cid = Math.random().toString(36).substr(2, 10);
-
 watch(
   () => props.stopPlay,
   (newVal) => {
-    if (newVal && curPlayId.value !== cid && curPlayId.value.length > 0 && audio.value.playing) {
+    if (newVal && curPlayId.value !== cid && curPlayId.value.length > 0 && state.value.playing) {
       pause();
     }
   }
 );
 
-const { curPlayId, changeCurPlayId } = inject('curPlayId');
-
-let url = ref('');
-let ref_audio = ref();
-let audio = ref({
-  // 音频是否处于播放状态的属性
-  playing: false,
-  // 音频当前播放时长
-  currentTime: 0,
-  // 音频最大播放时长
-  maxTime: 0,
-  isPlaying: false,
-  loading: false
-});
-
 const emits = defineEmits(['pauseOtherAudio', 'changeStopPlay']);
 
-GetFileStoreInfo({ file_id: props.fileId }).then(({ file_url, media_duration }) => {
-  url.value = file_url;
-  audio.value.maxTime = parseInt(media_duration);
-});
-
-// 播放音频
-function play() {
-  audio.value.playing = true;
-  audio.value.isPlaying = true;
-  changeCurPlayId(cid);
-  emits('pauseOtherAudio');
-  return ref_audio.value.play();
-}
-// 播放事件
-function onPlay() {
-  audio.value.playing = true;
-  audio.value.isPlaying = true;
-  emits('changeStopPlay');
-}
-
-// 暂停音频
-function pause() {
-  audio.value.playing = false;
-  audio.value.isPlaying = false;
-  return ref_audio.value.pause();
-}
-// 暂停事件
-function onPause() {
-  audio.value.playing = false;
-  audio.value.isPlaying = false;
-  emits('changeStopPlay');
-}
-
-// 持续时间改变事件
-function onDurationchange({ target }) {
-  audio.value.maxTime = parseInt(target.duration);
-}
-
-// 当音频当前时间改变后,进度条也要改变
-function onTimeupdate({ target }) {
-  audio.value.currentTime = target.currentTime;
-}
-
-// 音频加载完之后
-function onLoadedmetadata({ target }) {
-  audio.value.maxTime = parseInt(target.duration);
-}
-
-function onCanplaythrough() {
-  audio.value.loading = false;
-}
+let ref_audio = ref();
 
-// 播放到媒体的结束位置事件
-function onEnded() {
-  audio.value.playing = false;
-  audio.value.isPlaying = false;
-  audio.value.currentTime = 0;
-  emits('changeStopPlay');
-}
+const {
+  cid,
+  state,
+  url,
+  curPlayId,
+  play,
+  onPlay,
+  pause,
+  onPause,
+  onDurationchange,
+  onTimeupdate,
+  onLoadedmetadata,
+  onCanplaythrough,
+  onEnded
+} = usePlay(props.fileId, emits, ref_audio);
 </script>
 
 <style lang="scss" scoped>

+ 39 - 10
src/views/teacher/create_course/step_table/create_task/components/task_template/components/TemplateRecording.vue

@@ -29,8 +29,20 @@
               />
             </template>
             <template v-else-if="item.message_type === messageItem[2].message_type">
-              <video :src="item._file_url"></video>
+              <div class="avatar">T</div>
+              <VideoShow
+                :file-id="item.file_id"
+                :stop-play="stopPlay"
+                @pauseOtherAudio="pauseOtherAudio"
+                @changeStopPlay="changeStopPlay"
+              />
             </template>
+            <svg-icon
+              class-name="delete"
+              class="display-none"
+              icon-class="delete-current"
+              @click="deleteMessageItem(i)"
+            />
           </li>
         </ul>
       </template>
@@ -60,7 +72,7 @@
           :key="icon"
           :class="['press-sound', icon]"
           @mousedown="() => curRecord === audioAndVideo[0].type && startRecording()"
-          @click="() => (curRecord === audioAndVideo[1].type ? changVisible_video(true) : '')"
+          @click="() => (curRecord === audioAndVideo[1].type ? changVisible_video(true, { fn: getVideoData }) : '')"
           @mouseup="() => curRecord === audioAndVideo[0].type && closeRecording()"
         >
           <svg-icon :icon-class="icon" class-name="button-icon" />
@@ -78,6 +90,7 @@ import { taskData, messageItem } from '../../data/TaskData';
 import { fileUploadPrimordial } from '@/api/app.js';
 
 import AudioShow from './AudioShow.vue';
+import VideoShow from './VideoShow.vue';
 
 const props = defineProps({
   listName: {
@@ -107,10 +120,7 @@ let curTemplateData =
 
 let message_list = ref(curTemplateData.message_list);
 
-const { changVisible_video } = inject('visible_video');
-
-const { stopPlay, changeStopPlay, pauseOtherAudio } = inject('stopPlay');
-
+// 文本
 let text = ref('');
 function sendMessage() {
   message_list.value.push(Object.assign({}, messageItem[0], { text: text.value }));
@@ -118,6 +128,10 @@ function sendMessage() {
 }
 
 let { curRecord, selectRecord } = useRecordingPageData();
+
+const { stopPlay, changeStopPlay, pauseOtherAudio } = inject('stopPlay');
+
+// 录音
 let { startRecording, closeRecording, isRecording, blob } = soundRecording();
 
 watch(blob, (newVal) => {
@@ -133,6 +147,18 @@ watch(blob, (newVal) => {
     }
   });
 });
+
+// 录影
+const { changVisible_video } = inject('visible_video');
+
+function getVideoData(data) {
+  message_list.value.push(data);
+}
+
+// 删除信息列表项
+function deleteMessageItem(i) {
+  message_list.value.splice(i, 1);
+}
 </script>
 
 <style lang="scss" scoped>
@@ -140,7 +166,7 @@ $tip-color: #999;
 
 // 消息框
 .message-box {
-  height: 230px;
+  min-height: 230px;
   padding: 16px 0;
   background-color: #f4f4f4;
   border: 1px solid $border-color;
@@ -156,10 +182,11 @@ $tip-color: #999;
     width: 100%;
     height: 100%;
     padding: 0 16px;
-    overflow: auto;
 
     &-item {
       display: flex;
+      column-gap: 16px;
+      align-items: center;
       white-space: pre-wrap;
 
       .text {
@@ -188,9 +215,11 @@ $tip-color: #999;
         &:not(:first-child) {
           margin-top: 22px;
         }
+      }
 
-        display: flex;
-        column-gap: 16px;
+      &:hover .delete {
+        display: block;
+        cursor: pointer;
       }
     }
   }

+ 90 - 0
src/views/teacher/create_course/step_table/create_task/components/task_template/components/VideoShow.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="video-container">
+    <video
+      ref="ref_video"
+      :src="url"
+      preload="metadata"
+      @play="onPlay"
+      @pause="onPause"
+      @ended="onEnded"
+      @durationchange="onDurationchange"
+      @loadedmetadata="onLoadedmetadata"
+      @timeupdate="onTimeupdate"
+      @canplaythrough="onCanplaythrough"
+    ></video>
+    <svg-icon v-show="!state.playing" icon-class="play" class-name="play" @click="play" />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'VideoShow'
+};
+</script>
+
+<script setup>
+import { ref, watch } from 'vue';
+import { usePlay } from './play';
+
+const props = defineProps({
+  fileId: {
+    type: String,
+    required: true
+  },
+  stopPlay: {
+    type: Boolean,
+    required: true
+  }
+});
+
+watch(
+  () => props.stopPlay,
+  (newVal) => {
+    if (newVal && curPlayId.value !== cid && curPlayId.value.length > 0) {
+      pause();
+    }
+  }
+);
+const emits = defineEmits(['pauseOtherAudio', 'changeStopPlay']);
+
+let ref_video = ref();
+const {
+  cid,
+  state,
+  url,
+  curPlayId,
+  play,
+  onPlay,
+  pause,
+  onPause,
+  onDurationchange,
+  onTimeupdate,
+  onLoadedmetadata,
+  onCanplaythrough,
+  onEnded
+} = usePlay(props.fileId, emits, ref_video);
+</script>
+
+<style lang="scss" scoped>
+.video-container {
+  position: relative;
+  padding: 8px 12px;
+  cursor: pointer;
+  background-color: #fff;
+  border: 1px solid $border-color;
+  border-radius: 4px;
+
+  video {
+    width: 353px;
+    height: 221px;
+  }
+
+  .play {
+    position: absolute;
+    top: 106px;
+    left: 172px;
+    width: 36px;
+    height: 36px;
+  }
+}
+</style>

+ 101 - 1
src/views/teacher/create_course/step_table/create_task/components/task_template/components/play.js

@@ -1,5 +1,105 @@
-import { ref, provide } from 'vue';
+import { ref, provide, inject } from 'vue';
+import { GetFileStoreInfo } from '@/api/app.js';
 
+/**
+ * 播放属性与事件
+ * @param {String} file_id 文件id
+ * @param {Array} emits emit 列表
+ * @param {Object} el refs
+ */
+export function usePlay(file_id, emits, el) {
+  const cid = Math.random().toString(36).substr(2, 10);
+  let state = ref({
+    // 是否处于播放状态
+    playing: false,
+    // 当前播放时长
+    currentTime: 0,
+    // 最大播放时长
+    maxTime: 0,
+    isPlaying: false,
+    loading: false
+  });
+
+  const { curPlayId, changeCurPlayId } = inject('curPlayId');
+  let url = ref('');
+  GetFileStoreInfo({ file_id }).then(({ file_url, media_duration }) => {
+    url.value = file_url;
+    state.value.maxTime = parseInt(media_duration);
+  });
+
+  // 播放音频
+  function play() {
+    state.value.playing = true;
+    changeCurPlayId(cid);
+    emits('pauseOtherAudio');
+    return el.value.play();
+  }
+  // 播放事件
+  function onPlay() {
+    state.value.playing = true;
+    emits('changeStopPlay');
+  }
+  // 暂停
+  function pause() {
+    state.value.playing = false;
+    state.value.isPlaying = false;
+    return el.value.pause();
+  }
+  // 暂停事件
+  function onPause() {
+    state.value.playing = false;
+    state.value.isPlaying = false;
+    emits('changeStopPlay');
+  }
+
+  // 持续时间改变事件
+  function onDurationchange({ target }) {
+    state.value.maxTime = parseInt(target.duration);
+  }
+
+  // 当前时间改变事件
+  function onTimeupdate({ target }) {
+    state.value.currentTime = target.currentTime;
+  }
+
+  // 加载完之后
+  function onLoadedmetadata({ target }) {
+    state.value.maxTime = parseInt(target.duration);
+  }
+
+  // 浏览器估计它可以在不停止内容缓冲的情况下播放媒体直到结束
+  function onCanplaythrough() {
+    state.value.loading = false;
+  }
+
+  // 播放到媒体的结束位置事件
+  function onEnded() {
+    state.value.playing = false;
+    state.value.isPlaying = false;
+    state.value.currentTime = 0;
+    emits('changeStopPlay');
+  }
+
+  return {
+    cid,
+    state,
+    curPlayId,
+    url,
+    play,
+    onPlay,
+    pause,
+    onPause,
+    onDurationchange,
+    onTimeupdate,
+    onLoadedmetadata,
+    onCanplaythrough,
+    onEnded
+  };
+}
+
+/**
+ * 其他播放暂停
+ */
 export function useOtherPausePlay() {
   let stopPlay = ref(false); // 停止其他播放
   function changeStopPlay() {

+ 4 - 2
src/views/teacher/create_course/step_table/create_task/components/task_template/components/recording.js

@@ -164,15 +164,17 @@ export function videoRecording(el) {
     changVisible_video
   });
 
-  function videoFileId(id, url) {
+  function sendVideo(data) {
     visible_video.value = false;
+    selectFn(data);
+    if (selectFn) selectFn = null;
   }
 
   return {
     blob,
     isRecording,
     visible_video,
-    videoFileId,
+    sendVideo,
     startVideo,
     closeVideo
   };

+ 1 - 2
src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/SubTask.vue

@@ -74,7 +74,7 @@
 <script setup>
 import { ref, inject } from 'vue';
 import { useDrag, customTypeList } from '../../util/drag';
-import { taskClassify, useTaskClassify } from '../../data/TaskClassify.js';
+import { taskClassify, getTaskClassifyAttr } from '../../data/TaskClassify.js';
 import { taskData } from '../../data/TaskData';
 import { useSubtaskItem } from './data';
 
@@ -97,7 +97,6 @@ let listName = inject('listName');
 let taskIndex = inject('taskIndex');
 
 let curSubtaskObj = taskData.value[listName][taskIndex].child_task_list[props.subtaskIndex];
-const { getTaskClassifyAttr } = useTaskClassify();
 
 function deleteSubtask() {
   emit('deleteSubtask', props.subtaskIndex);

+ 1 - 3
src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/SubtaskItem.vue

@@ -16,7 +16,7 @@
 </template>
 
 <script setup>
-import { useTaskClassify } from '../../data/TaskClassify.js';
+import { getTaskClassifyAttr } from '../../data/TaskClassify.js';
 
 const props = defineProps({
   itemIndex: {
@@ -31,8 +31,6 @@ const props = defineProps({
 
 const emits = defineEmits(['deleteSubtaskItem']);
 
-const { getTaskClassifyAttr } = useTaskClassify();
-
 function deleteSubtaskItem() {
   emits('deleteSubtaskItem', props.itemIndex);
 }

+ 1 - 3
src/views/teacher/create_course/step_table/create_task/components/task_template/subtask/data.js

@@ -1,6 +1,6 @@
 import { subtaskObj, subtaskInfoBlock } from '../../data/TaskData';
 import { Message } from 'element-ui';
-import { taskClassify, useTaskClassify } from '../../data/TaskClassify';
+import { taskClassify, getTaskClassifyAttr } from '../../data/TaskClassify';
 
 /**
  * 处理子任务
@@ -28,8 +28,6 @@ export function useSubtask(subtaskList) {
  * @param {Object} info_block_list ref格式的信息块列表
  */
 export function useSubtaskItem(info_block_list) {
-  const { getTaskClassifyAttr } = useTaskClassify();
-
   /**
    * 添加子任务项
    * @param {String} taskClassifyType 任务分类

+ 28 - 15
src/views/teacher/create_course/step_table/create_task/index.vue

@@ -35,16 +35,27 @@
         <svg-icon icon-class="write" class-name="write" @click="editorClassName(true)" />
       </div>
       <div class="class-title-operation">
-        <div class="preview"><svg-icon icon-class="preview" /><span class="button-name">预览</span></div>
-        <div class="preserve" @click="saveCSItem(cs_item_id)">
-          <svg-icon icon-class="preserve" /><span class="button-name">保存</span>
-        </div>
+        <template v-if="curPageState === pageStateList[0]">
+          <div class="title-button" @click="task.changeTaskType(pageStateList[1])">
+            <svg-icon icon-class="preview" /><span class="button-name">预览</span>
+          </div>
+          <div class="title-button primary" @click="saveCSItem(cs_item_id)">
+            <svg-icon icon-class="preserve" /><span class="button-name">保存</span>
+          </div>
+        </template>
+        <template v-else>
+          <div class="title-button"><el-switch v-model="isMindMapping" :width="30" />思维导图模式</div>
+          <div class="title-button" @click="changeCurPageState(pageStateList[0])">
+            <svg-icon icon-class="course-close" />
+            <span class="button-name">结束预览</span>
+          </div>
+        </template>
       </div>
     </div>
     <!-- 创建课节 -->
-    <CreateClassSection v-if="isCreateClassSection" />
+    <CreateClassSection v-show="isCreateClassSection" />
     <!-- 课节主体内容 -->
-    <TaskEditor v-else />
+    <TaskEditor v-show="!isCreateClassSection" ref="task" />
   </div>
 </template>
 
@@ -52,6 +63,7 @@
 import { ref } from 'vue';
 import { useCSItem, initData } from './index';
 import { saveCSItem } from './components/data/TaskData';
+import { curPageState, isMindMapping, changeCurPageState, pageStateList } from './components/data/TaskType';
 
 import TaskEditor from './components/layouts/TaskEditor.vue';
 import CreateClassSection from './components/layouts/createClassSection.vue';
@@ -70,6 +82,8 @@ const {
 } = useCSItem(content);
 
 initData();
+
+let task = ref();
 </script>
 
 <style lang="scss" scoped>
@@ -171,10 +185,12 @@ $basic-background-color: #f7f7f7;
 
     &-operation {
       display: flex;
+      column-gap: 8px;
       align-items: center;
+      padding-right: 8px;
 
-      %preview,
-      .preview {
+      %title-button,
+      .title-button {
         display: flex;
         column-gap: 6px;
         align-items: center;
@@ -189,14 +205,11 @@ $basic-background-color: #f7f7f7;
         background-color: #fff;
         border: 1px solid $border-color;
         border-radius: 4px;
-      }
 
-      .preserve {
-        @extend %preview;
-
-        margin: 0 8px;
-        color: #fff;
-        background-color: #5498ff;
+        &.primary {
+          color: #fff;
+          background-color: #5498ff;
+        }
       }
     }
   }