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

1、课节预览。2、正确答案、参考答案与解析

dsy 2 недель назад
Родитель
Сommit
5eed9b251f
36 измененных файлов с 3171 добавлено и 365 удалено
  1. 5 5
      src/api/app.js
  2. BIN
      src/assets/component/answer-analysis-icon.png
  3. BIN
      src/assets/component/answer-analysis.png
  4. BIN
      src/assets/component/component-answer.png
  5. BIN
      src/assets/component/component-correct.png
  6. BIN
      src/assets/component/component-retry.png
  7. BIN
      src/assets/component/correct-answer-icon.png
  8. BIN
      src/assets/component/correct-answer.png
  9. BIN
      src/assets/component/reference-answer-icon.png
  10. BIN
      src/assets/component/reference-answer.png
  11. 1801 0
      src/courseware_preview/index.vue
  12. 6 2
      src/enterPage.vue
  13. 8 0
      src/router/modules/basic.js
  14. 2 2
      src/router/modules/index.js
  15. 66 0
      src/utils/directive.js
  16. 269 0
      src/views/book/courseware/preview/common/AnswerAnalysis.vue
  17. 66 0
      src/views/book/courseware/preview/common/PreviewOperation.vue
  18. 15 0
      src/views/book/courseware/preview/components/character/CharacterPreview.vue
  19. 77 68
      src/views/book/courseware/preview/components/character_structure/CharacterStructurePreview.vue
  20. 2 2
      src/views/book/courseware/preview/components/common/AudioPlay.vue
  21. 34 1
      src/views/book/courseware/preview/components/common/PreviewMixin.js
  22. 102 4
      src/views/book/courseware/preview/components/fill/FillPreview.vue
  23. 34 26
      src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue
  24. 8 0
      src/views/book/courseware/preview/components/input/InputPreview.vue
  25. 61 1
      src/views/book/courseware/preview/components/judge/JudgePreview.vue
  26. 10 2
      src/views/book/courseware/preview/components/matching/MatchingPreview.vue
  27. 195 178
      src/views/book/courseware/preview/components/newWord_template/NewWordTemplatePreview.vue
  28. 50 43
      src/views/book/courseware/preview/components/pinyin_base/PinyinBasePreview.vue
  29. 7 0
      src/views/book/courseware/preview/components/record_input/RecordInputPreview.vue
  30. 1 0
      src/views/book/courseware/preview/components/record_input/SoundRecord.vue
  31. 55 2
      src/views/book/courseware/preview/components/select/SelectPreview.vue
  32. 23 18
      src/views/book/courseware/preview/components/sort/SortPreview.vue
  33. 250 11
      src/views/book/courseware/preview/components/table/TablePreview.vue
  34. 7 0
      src/views/book/courseware/preview/components/video_interaction/VideoInteractionPreview.vue
  35. 8 0
      src/views/book/courseware/preview/components/voice_matrix/VoiceMatrixPreview.vue
  36. 9 0
      src/web_preview/index.vue

+ 5 - 5
src/api/app.js

@@ -101,23 +101,23 @@ export function LearnWebSI(MethodName, data) {
  * @param {object} data 请求数据
  * @param {string} data.book_id 教材 ID
  */
-export function CheckBookPreviewIdentityCode (data) {
+export function CheckBookPreviewIdentityCode(data) {
   return http.post(
     `${process.env.VUE_APP_EepServer}?MethodName=book_preview_manager-CheckBookPreviewIdentityCode`,
     data,
   );
 }
 
- /**
+/**
  * 校验识别码
  * @param {object} data 请求数据
  * @param {string} data.identity_code 识别码
  * @param {string} data.check_code 校验码
  * @param {string} data.app_id 应用标识
  * @param {string} data.user_name 用户名
-  */
-export function obocCheckIdentityCode (data) {
-    return http.post(`${process.env.VUE_APP_EepServer}?MethodName=oboc-CheckIdentityCode`, data);
+ */
+export function obocCheckIdentityCode(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=oboc-CheckIdentityCode`, data);
 }
 
 /**

BIN
src/assets/component/answer-analysis-icon.png


BIN
src/assets/component/answer-analysis.png


BIN
src/assets/component/component-answer.png


BIN
src/assets/component/component-correct.png


BIN
src/assets/component/component-retry.png


BIN
src/assets/component/correct-answer-icon.png


BIN
src/assets/component/correct-answer.png


BIN
src/assets/component/reference-answer-icon.png


BIN
src/assets/component/reference-answer.png


+ 1801 - 0
src/courseware_preview/index.vue

@@ -0,0 +1,1801 @@
+<template>
+  <div class="common-preview">
+    <div class="common-preview__header">
+      <div class="menu-container">
+        {{ courseware_info.book_name }}
+      </div>
+      <div class="courseware">
+        <div class="permission-control">
+          <span class="student"> 学生:张三 </span>
+          <el-checkbox v-model="permissionControl.can_answer">可作答</el-checkbox>
+          <el-checkbox v-model="permissionControl.can_judge_correct">可判断对错(客观题)</el-checkbox>
+          <el-checkbox v-model="permissionControl.can_show_answer">可查看答案</el-checkbox>
+          <el-checkbox v-model="permissionControl.can_correct">可批改</el-checkbox>
+          <el-checkbox v-model="permissionControl.can_check_correct">可查看批改</el-checkbox>
+        </div>
+        <!-- <span class="link">
+          <el-select v-model="lang" placeholder="请选择语言" size="mini" class="lang-select">
+            <el-option v-for="item in langList" :key="item.type" :label="item.name" :value="item.type" />
+          </el-select>
+        </span>
+        <span class="link">
+          <el-checkbox v-model="chinese" true-label="zh-Hant" false-label="zh-Hans">繁体</el-checkbox>
+        </span> -->
+        <div class="operator">
+          <slot name="operator" :courseware="courseware_info"></slot>
+          <span class="link">提交答案</span>
+          <span class="link">读取答案</span>
+          <span class="link">提交批改</span>
+          <span class="link">读取批改</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="audit-content">
+      <div ref="previewMain" class="main-container" :style="{ paddingRight: sidebarShow ? '15px' : '315px' }">
+        <main :class="['preview-main']">
+          <div class="preview-left"></div>
+          <CoursewarePreview
+            v-if="courseware_info.book_name"
+            ref="courserware"
+            :is-show-group="isShowGroup"
+            :group-show-all="groupShowAll"
+            :group-row-list="content_group_row_list"
+            :data="data"
+            :courseware-id="curSelectId"
+            :component-list="component_list"
+            :background="background"
+            :can-remark="isTrue(courseware_info.is_my_audit_task) && isTrue(courseware_info.is_can_add_audit_remark)"
+            :show-remark="false"
+            :project="project"
+            :component-remark-obj="remark_list_obj"
+            @computeScroll="computeScroll"
+            @editNote="handEditNote"
+            @saveCollect="saveCollect"
+          />
+          <div class="preview-right"></div>
+        </main>
+
+        <!-- 右侧菜单栏 - 收缩 -->
+        <aside v-if="!sidebarShow && !isFullScreen" class="sidebar-bar">
+          <aside class="toolbar">
+            <div class="toolbar-special">
+              <img :src="require('@/assets/icon/sidebar-fullscreen.png')" alt="全屏" @click="fullScreen" />
+              <img :src="require('@/assets/icon/sidebar-toolkit.png')" alt="工具箱" />
+              <img :src="require(`@/assets/icon/arrow-down.png`)" alt="伸缩" @click="toggleSidebarShow" />
+            </div>
+          </aside>
+        </aside>
+      </div>
+
+      <div v-if="!sidebarShow" class="back-top" @click="backTop">
+        <img :src="require(`@/assets/icon/back-top.png`)" alt="返回顶部" />
+      </div>
+
+      <!-- 右侧工具栏 -->
+      <aside v-if="sidebarShow" ref="sidebarMenu" class="sidebar">
+        <aside class="toolbar">
+          <div class="toolbar-special">
+            <img :src="require('@/assets/icon/sidebar-fullscreen.png')" alt="全屏" @click="fullScreen" />
+            <img :src="require('@/assets/icon/sidebar-toolkit.png')" alt="工具箱" />
+          </div>
+          <div v-if="sidebarShow" class="toolbar-list">
+            <div
+              v-for="{ icon, title, handle, param, children } in sidebarIconList"
+              :key="icon"
+              :class="['sidebar-item', { active: curToolbarIcon === icon }]"
+              :title="title"
+              @click="handleSidebarClick(handle, param, icon, children)"
+            >
+              <div
+                class="sidebar-icon icon-mask"
+                :style="{
+                  backgroundColor: curToolbarIcon === icon ? '#fff' : '#1E2129',
+                  maskImage: `url(${require(`@/assets/icon/sidebar-${icon}.svg`)})`,
+                }"
+              ></div>
+            </div>
+          </div>
+          <div class="adjustable" @click="toggleSidebarShow">
+            <img :src="require(`@/assets/icon/arrow-up.png`)" alt="伸缩" />
+          </div>
+        </aside>
+        <div class="content">
+          <div v-if="curToolbarIcon === 'search'" class="resource_box">
+            <h5>{{ drawerTitle }}</h5>
+            <div style="height: 40px"></div>
+            <el-row :gutter="10" style="margin: 5px">
+              <el-col :span="16">
+                <el-input v-model="searchContent" placeholder="请输入文本内容" clearable />
+              </el-col>
+              <el-col :span="4">
+                <el-button type="primary" @click="querySearchList"> 查询 </el-button>
+              </el-col>
+            </el-row>
+            <div>
+              <el-table :data="searchList" :show-header="false">
+                <!-- <el-table-column prop="courseware_name" label="课件" />
+                <el-table-column prop="component_type" label="组件" /> -->
+                <el-table-column>
+                  <template #default="{ row }">
+                    {{ row.courseware_name + ' / ' + row.component_type_name }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="" width="50">
+                  <template #default="{ row }">
+                    <el-link type="primary" @click="handleLocation(row, 3)">定位</el-link>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </div>
+          <!-- <div v-if="curToolbarIcon === 'totalResources'" class="resource_box"></div> -->
+          <div v-if="['image', 'audio', 'video'].includes(twoCurToolbarIcon)" class="resource_box">
+            <div class="source-toolbar-list">
+              <div
+                v-for="{ icon, title, handle, param } in twoSidebarList"
+                :key="icon"
+                :class="['sidebar-item', { active: twoCurToolbarIcon === icon }]"
+                :title="title"
+                @click="handleSidebarClick(handle, param, icon, '', 2)"
+              >
+                <div
+                  class="sidebar-icon icon-mask"
+                  :style="{
+                    backgroundColor: twoCurToolbarIcon === icon ? '#fff' : '#1E2129',
+                    maskImage: `url(${require(`@/assets/icon/sidebar-${icon}.svg`)})`,
+                  }"
+                ></div>
+              </div>
+            </div>
+            <div style="height: 40px"></div>
+
+            <el-collapse v-model="activeBookChapterId" accordion @change="multimediaHandleChange">
+              <el-collapse-item
+                v-for="chapter in bookChapterList"
+                :key="chapter.id"
+                :name="chapter.id"
+                :title="chapter.name"
+              >
+                <!-- 加载状态 -->
+                <div v-if="multimediaLoadingStates" class="loading-text">加载中...</div>
+
+                <!-- 加载完成显示数据 -->
+                <div v-else-if="chapter.data">
+                  <ul class="scroll-container" infinite-scroll-disabled="disabled" :infinite-scroll-immediate="false">
+                    <li
+                      v-for="(item, index) in chapter.data"
+                      :key="`${chapter.id}-${index}`"
+                      class="list-item"
+                      @click="handleFileClick(item?.courseware_id, item?.component_id)"
+                    >
+                      <template v-if="parseInt(drawerType) === 0">
+                        <el-image v-if="shouldShowItem(chapter, item)" :src="item.file_url" fit="contain" />
+                        <div class="mark">
+                          <span class="word">{{ item.file_name }}</span>
+                          <el-link type="primary" class="el-icon-place linkLocation" @click="handleLocation(item, 3)" />
+                        </div>
+                      </template>
+                      <template v-else-if="parseInt(drawerType) === 1">
+                        <AudioPlay
+                          v-if="shouldShowItem(chapter, item)"
+                          view-size="middle"
+                          :file-id="item.file_id"
+                          :file-name="item.file_name.slice(0, item.file_name.lastIndexOf('.'))"
+                          :show-slider="true"
+                          :audio-index="index"
+                        />
+                        <div class="mark">
+                          <span class="word"></span>
+                          <el-link type="primary" class="el-icon-place linkLocation" @click="handleLocation(item, 3)" />
+                        </div>
+                      </template>
+                      <template v-else-if="parseInt(drawerType) === 2">
+                        <VideoPlay
+                          v-if="shouldShowItem(chapter, item)"
+                          view-size="small"
+                          :file-id="item.file_id"
+                          :video-index="index"
+                        />
+                        <div class="mark">
+                          <span class="word">{{ item.file_name }}</span>
+                          <el-link type="primary" class="el-icon-place linkLocation" @click="handleLocation(item, 3)" />
+                        </div>
+                      </template>
+                    </li>
+                  </ul>
+                </div>
+
+                <!-- 加载失败或未加载 -->
+                <div v-else class="error-text">没有资源</div>
+              </el-collapse-item>
+            </el-collapse>
+          </div>
+
+          <div v-if="curToolbarIcon === 'collect'" class="resource_box">
+            <h5>{{ drawerTitle }}</h5>
+            <div style="height: 40px"></div>
+            <ul v-if="allCottectList.length > 0" class="card-box">
+              <li v-for="item in allCottectList" :key="item.id">
+                <span class="el-icon-notebook-2"> 原文</span>
+                <span>{{ item.text }}</span>
+                <div>
+                  <el-button type="text" class="el-icon-delete" @click="handDelCollect(item.id)"> 删除</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-place" @click="handleLocation(item, 2)"> 定位</el-button>
+                </div>
+              </li>
+            </ul>
+          </div>
+          <div v-if="curToolbarIcon === 'note'" class="resource_box">
+            <h5>{{ drawerTitle }}</h5>
+            <div style="height: 40px"></div>
+            <ul v-if="allNoteList.length > 0" class="card-box">
+              <li v-for="item in allNoteList" :key="item.id">
+                <span class="el-icon-notebook-2"> 原文</span>
+                <span>{{ item.text }}</span>
+                <el-divider class="mt10" />
+                <span v-html="item.note"></span>
+                <div>
+                  <el-button type="text" class="el-icon-edit" @click="handEditNote(item)"> 编辑</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-delete" @click="handDelNote(item.id)"> 删除</el-button>
+                  <el-divider direction="vertical" />
+                  <el-button type="text" class="el-icon-place" @click="handleLocation(item, 1)"> 定位</el-button>
+                </div>
+              </li>
+            </ul>
+          </div>
+          <template v-if="curToolbarIcon === 'audit'">
+            <AuditRemark :remark-list="remark_list" :is-audit="isShowAudit" @deleteRemarks="deleteRemarks" />
+          </template>
+        </div>
+
+        <div class="back-top" @click="backTop">
+          <img :src="require(`@/assets/icon/back-top.png`)" alt="返回顶部" />
+        </div>
+      </aside>
+    </div>
+
+    <el-dialog title="" :visible="visibleMindMap" width="1100px" class="audit-dialog" @close="dialogClose('MindMap')">
+      <MindMap
+        v-if="isChildDataLoad"
+        ref="mindMapRef"
+        :project-id="projectId"
+        :mind-map-json-data="mindMapJsonData"
+        @child-click="handleNodeClick"
+      />
+    </el-dialog>
+
+    <el-dialog
+      title=""
+      :visible="visibleVisNetwork"
+      width="1100px"
+      class="audit-dialog"
+      :close-on-click-modal="false"
+      @close="dialogClose('VisNetwork')"
+    >
+      <VisNetwork ref="visNetworkRef" :book-id="projectId" @child-click="handleNodeClick" />
+    </el-dialog>
+
+    <ExplanatoryNoteDialog
+      ref="explanatoryNote"
+      :open.sync="editDialogOpen"
+      :init-data="oldRichData"
+      title-text="笔记"
+      @confirm="saveNote"
+      @cancel="delNote"
+    />
+  </div>
+</template>
+
+<script>
+import CoursewarePreview from '@/views/book/courseware/preview/CoursewarePreview.vue';
+import { isTrue } from '@/utils/validate';
+import MindMap from '@/components/MindMap.vue';
+import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
+import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPlay.vue';
+import ExplanatoryNoteDialog from '@/components/ExplanatoryNoteDialog.vue';
+import VisNetwork from '@/components/VisNetwork.vue';
+import * as OpenCC from 'opencc-js';
+
+import { GetBookCoursewareInfo, GetCoursewareAuditRemarkList, GetProjectInfo } from '@/api/project';
+import {
+  ContentGetCoursewareContent_View,
+  ChapterGetBookChapterStructExpandList,
+  GetBookBaseInfo,
+  MangerGetBookMindMap,
+  GetBookChapterStructExpandList,
+  PageQueryBookResourceList,
+  GetLanguageTypeList,
+  GetBookUnifiedAttrib,
+  GetMyNoteList,
+  DeleteMyNote,
+  AddMyNote,
+  UpdateMyNote,
+  AddMyCollect,
+  GetMyCollectList,
+  DeleteMyCollect,
+  SearchBookContentText,
+} from '@/api/book';
+import { getLocalStore } from '@/utils/auth';
+import { toggleFullScreen } from '@/utils/common';
+
+export default {
+  name: 'CommonPreview',
+  components: {
+    CoursewarePreview,
+    MindMap,
+    VideoPlay,
+    AudioPlay,
+    ExplanatoryNoteDialog,
+    VisNetwork,
+  },
+  provide() {
+    return {
+      getLang: () => this.lang,
+      getChinese: () => this.chinese,
+      getLangList: () => this.langList,
+      convertText: this.convertText,
+      getProjectId: () => this.projectId,
+      getSelectId: () => this.select_node,
+      getTitleList: () => this.title_list,
+      getPermissionControl: () => this.permissionControl,
+    };
+  },
+  data() {
+    const sidebarIconList = [
+      // { icon: 'search', title: '搜索', handle: 'getSearch', param: { type: '5' } },
+      // { icon: 'mindmap', title: '思维导图', handle: 'openMindMap', param: {} },
+      // { icon: 'knowledge', title: '知识图谱', handle: 'openVisNetwork', param: {} },
+      // {
+      //   icon: 'totalResources',
+      //   title: '总资源',
+      //   handle: '',
+      //   param: {},
+      //   children: [
+      //     { icon: 'audio', title: '音频', handle: 'openDrawer', param: { type: '1' } },
+      //     { icon: 'image', title: '图片', handle: 'openDrawer', param: { type: '0' } },
+      //     { icon: 'video', title: '视频', handle: 'openDrawer', param: { type: '2' } },
+      //   ],
+      // },
+      // { icon: 'collect', title: '收藏', handle: 'getCollect', param: { type: '3' } },
+      // { icon: 'note', title: '笔记', handle: 'getNote', param: { type: '4' } },
+      // { icon: 'translate', title: '翻译', handle: '', param: {} },
+      // { icon: 'setting', title: '设置', handle: '', param: {} },
+    ];
+
+    return {
+      id: getLocalStore('courseware_id'),
+      projectId: getLocalStore('book_id') || '',
+      task_id: getLocalStore('task_id') || '',
+      select_node: this.id,
+      courseware_info: {
+        book_name: '',
+        is_can_start_edit: 'false',
+        is_can_submit_audit: 'false',
+        is_can_audit_pass: 'false',
+        is_can_audit_reject: 'false',
+        is_can_add_audit_remark: 'false',
+        is_can_finish_audit: 'false',
+        is_can_request_shangjia_book: 'false',
+        is_can_request_rollback_project: 'false',
+        is_can_shangjia_book: 'false',
+        is_can_rollback_project: 'false',
+      },
+      background: {
+        background_image_url: '',
+        background_position: {
+          left: 0,
+          top: 0,
+        },
+      },
+      node_list: [],
+      data: { row_list: [] },
+      component_list: [],
+      content_group_row_list: [],
+      remark_list: [],
+      remark_list_obj: {}, // 存放以组件为对象的数组
+      searchList: [],
+      searchContent: '',
+      visible: false,
+      remark_content: '',
+      submit_loading: false,
+      isTrue,
+      menuPosition: {
+        x: -1,
+        y: -1,
+        componentId: 'WHOLE',
+      },
+      curToolbarIcon: this.isShowAudit ? 'audit' : '',
+      sidebarIconList,
+      twoSidebarList: [],
+      twoCurToolbarIcon: '',
+      visibleMindMap: false,
+      visibleVisNetwork: false,
+      isChildDataLoad: false,
+      mindMapJsonData: {}, // 思维导图json数据
+      drawerType: '', // 抽屉类型
+      drawerStyle: {
+        top: '0',
+        height: '0',
+        right: '0',
+      },
+      page_capacity: 10,
+      cur_page: 1,
+      file_list: [],
+      total_count: 0,
+      loading: false,
+      lastLoadTime: 0,
+      minLoadInterval: 3 * 1000,
+      isShowGroup: false,
+      groupShowAll: true,
+      opencc: OpenCC.Converter({ from: 'cn', to: 'tw' }),
+      langList: [],
+      lang: 'ZH',
+      chinese: 'zh-Hans',
+      isJudgeCorrect: false,
+      isShowAnswer: false,
+      unified_attrib: {},
+      curSelectId: this.id,
+      sidebarShow: true,
+      project: {
+        editor: '', // 作者
+        cover_image_file_id: null, // 封面图片ID
+        cover_image_file_url: '', // 封面图片URL
+      },
+      allNoteList: [],
+      editDialogOpen: false,
+      oldRichData: {},
+      newSelectedInfo: null,
+      allCottectList: [],
+      bookChapterList: [],
+      book_id: '',
+      activeBookChapterId: '',
+      multimediaLoadingStates: true,
+      isFullScreen: false, // 是否全屏状态
+      title_list: [],
+      // 模拟答题权限控制
+      permissionControl: {
+        can_answer: false, // 可作答
+        can_judge_correct: false, // 可判断对错(客观题)
+        can_show_answer: false, // 可查看答案
+        can_correct: false, // 可批改
+        can_check_correct: false, // 可查看批改
+      },
+    };
+  },
+  computed: {
+    disabled() {
+      const result = this.loading || this.noMore;
+      return result;
+    },
+    noMore() {
+      const result = this.file_list.length >= this.total_count;
+      return result;
+    },
+    drawerTitle() {
+      const titleMap = {
+        0: '图片资源',
+        1: '音频资源',
+        2: '视频资源',
+        3: '收藏列表',
+        4: '笔记列表',
+        5: '搜索结果',
+      };
+      return titleMap[this.drawerType] || '资源列表';
+    },
+    shouldShowItem() {
+      return (chapter, item) => {
+        return this.activeBookChapterId === chapter.id && item && item.file_id;
+      };
+    },
+  },
+  watch: {
+    isJudgeCorrect(newVal) {
+      if (!newVal) {
+        this.isShowAnswer = false;
+      }
+      this.simulateAnswer(newVal);
+    },
+    isShowAnswer() {
+      this.simulateAnswer();
+    },
+    curSelectId() {
+      if (this.curToolbarIcon === 'note') {
+        this.getNote();
+      } else if (this.curToolbarIcon === 'collect') {
+        this.getCollect();
+      }
+    },
+  },
+  mounted() {
+    this.calcDrawerPosition();
+  },
+  created() {
+    this.getBookBaseInfo();
+    this.getBookChapterStructExpandList();
+    this.getCoursewareComponentContent_View(this.id);
+    this.getBookUnifiedAttr();
+    this.getProjectInfo();
+    // 监听全屏事件
+    document.addEventListener('fullscreenchange', () => {
+      if (document.fullscreenElement) {
+        this.isFullScreen = true;
+      } else {
+        this.isFullScreen = false;
+      }
+    });
+  },
+  beforeDestroy() {
+    document.removeEventListener('fullscreenchange', () => {});
+  },
+  methods: {
+    selectFirstLeafNode() {
+      if (!this.node_list || this.node_list.length === 0) return;
+      let node = this.node_list.find((x) => this.isTrue(x.is_leaf_chapter));
+      if (!node) return;
+      this.selectChapterNode(node.id, true);
+    },
+    getBookBaseInfo() {
+      GetBookBaseInfo({ id: this.projectId }).then(({ book_info }) => {
+        this.courseware_info = { ...this.courseware_info, ...book_info, book_name: book_info.name };
+        if (book_info.cover_image_file_id) {
+          this.project.cover_image_file_url = book_info.cover_image_file_url;
+        }
+        if (book_info.editor) {
+          this.project.editor = book_info.editor;
+        }
+        if (book_info.cover_image_file_url) {
+          this.project.cover_image_file_url = book_info.cover_image_file_url;
+        }
+      });
+    },
+
+    getProjectInfo() {
+      GetProjectInfo({ id: this.projectId }).then(({ project_info }) => {
+        if (project_info.cover_image_file_url) {
+          this.project = project_info;
+        }
+      });
+    },
+
+    /**
+     * 得到教材课件信息
+     * @param {string} id - 课件ID
+     */
+    getBookCoursewareInfo(id) {
+      GetBookCoursewareInfo({ id, is_contain_producer: 'true', is_contain_auditor: 'true' }).then(
+        ({ courseware_info }) => {
+          this.courseware_info = { ...this.courseware_info, ...courseware_info };
+          this.getLangList();
+        },
+      );
+    },
+    /**
+     * 得到课件内容(展示内容)
+     * @param {string} id - 课件ID
+     */
+    getCoursewareComponentContent_View(id) {
+      ContentGetCoursewareContent_View({ id }).then(
+        ({ content, component_list, content_group_row_list, title_list }) => {
+          if (title_list) this.title_list = title_list || [];
+          if (content) {
+            const _content = JSON.parse(content);
+            this.data = _content;
+            this.background = {
+              background_image_url: _content.background_image_url,
+              background_position: _content.background_position,
+            };
+          } else {
+            this.data = { row_list: [] };
+          }
+
+          if (component_list) this.component_list = component_list;
+          this.component_list.forEach((x) => {
+            if (x.component_type === 'audio') {
+              let _c = JSON.parse(x.content);
+              let p = _c.property || {};
+              if (!p.file_name_display_mode) p.file_name_display_mode = 'true';
+              if (p.view_method === 'independent' && !p.style_mode) {
+                p.style_mode = 'middle';
+              }
+              if (!p.style_mode) p.style_mode = 'big';
+              if (p.view_method === 'icon') {
+                p.file_name_display_mode = 'false';
+                p.view_method = 'independent';
+                p.style_mode = 'small';
+              }
+
+              x.content = JSON.stringify(_c);
+            } else if (x.component_type === 'richtext') {
+              let _c = JSON.parse(x.content);
+              let p = _c.property || {};
+              let lev = Number(p.title_style_level);
+              if (p.is_title !== 'true' || lev < 1 || !_c.content) return;
+
+              let style = title_list.find((y) => y.level === lev) || {};
+              if (style && style.style) {
+                style = JSON.parse(style.style);
+                let c_text = _c.content;
+
+                const parser = new DOMParser();
+                const doc = parser.parseFromString(c_text, 'text/html');
+                const body = doc.body;
+                const pElements = body.querySelectorAll('p');
+                this.processHtmlString(pElements, style);
+                _c.content = body.innerHTML;
+
+                x.content = JSON.stringify(_c);
+              }
+            }
+          });
+
+          if (content_group_row_list) this.content_group_row_list = JSON.parse(content_group_row_list) || [];
+        },
+      );
+    },
+    processHtmlString(pElements, styleConfig, isClear) {
+      pElements.forEach((pElement) => {
+        if (isClear) {
+          pElement.removeAttribute('style');
+        } else {
+          const style = pElement.style;
+
+          if (styleConfig.font) style.fontFamily = styleConfig.font;
+          if (styleConfig.font_size) style.fontSize = styleConfig.font_size;
+          if (styleConfig.text_color) style.color = styleConfig.text_color;
+
+          style.fontWeight = styleConfig.bold === 'true' ? 'bold' : 'normal';
+          style.fontStyle = styleConfig.italic === 'true' ? 'italic' : 'normal';
+          if (styleConfig.indent || styleConfig.indent === 0) style.marginLeft = `${styleConfig.indent}px`;
+        }
+      });
+    },
+    getLangList() {
+      GetLanguageTypeList({ book_id: this.courseware_info.book_id, is_contain_zh: 'true' }).then(
+        ({ language_type_list }) => {
+          this.langList = language_type_list;
+        },
+      );
+    },
+
+    /**
+     * 得到教材章节结构展开列表
+     */
+    getBookChapterStructExpandList() {
+      ChapterGetBookChapterStructExpandList({
+        book_id: this.projectId,
+        node_deep_mode: 0,
+        is_contain_producer: 'true',
+        is_contain_auditor: 'true',
+      }).then(({ node_list }) => {
+        this.node_list = node_list;
+      });
+    },
+
+    getBookUnifiedAttr() {
+      GetBookUnifiedAttrib({ book_id: this.projectId }).then(({ content }) => {
+        if (content) {
+          this.unified_attrib = JSON.parse(content);
+        }
+      });
+    },
+
+    /**
+     * 选择节点
+     * @param {string} nodeId - 节点ID
+     */
+    selectNode(nodeId) {
+      this.getCoursewareComponentContent_View(nodeId);
+      this.getBookCoursewareInfo(nodeId);
+      this.getCoursewareAuditRemarkList(nodeId);
+      this.select_node = nodeId;
+    },
+    // 审校批注列表
+    getCoursewareAuditRemarkList(id) {
+      this.remark_list = [];
+      let remarkListObj = {};
+      GetCoursewareAuditRemarkList({
+        courseware_id: id,
+      }).then(({ remark_list }) => {
+        this.remark_list = remark_list;
+        if (!remark_list) return;
+        remarkListObj = remark_list.reduce((acc, item) => {
+          if (!acc[item.component_id]) {
+            acc[item.component_id] = [];
+          }
+          acc[item.component_id].push(item);
+          return acc;
+        }, {});
+
+        this.remark_list_obj = remarkListObj;
+      });
+    },
+    dialogClose(type) {
+      this[`visible${type}`] = false;
+    },
+    // 计算previewMain滑动距离
+    computeScroll() {
+      this.$refs.courserware.handleResult(
+        this.$refs.previewMain.scrollTop,
+        this.$refs.previewMain.scrollLeft,
+        this.select_node,
+      );
+    },
+
+    /**
+     * 处理侧边栏图标点击事件
+     * @param {string} handle - 处理函数名
+     * @param {any} param - 处理函数参数
+     * @param {string} icon - 图标名称
+     */
+    handleSidebarClick(handle, param, icon, children, barLevel) {
+      if (typeof handle === 'string' && handle && typeof this[handle] === 'function') {
+        this[handle](param);
+      }
+
+      if (barLevel === 2) {
+        this.twoCurToolbarIcon = icon;
+      } else {
+        this.curToolbarIcon = icon;
+        this.twoCurToolbarIcon = '';
+      }
+
+      if (children && children.length > 0 && Array.isArray(children)) {
+        this.twoSidebarList = children;
+        this.twoCurToolbarIcon = children[0].icon;
+        this.openDrawer(children[0].param);
+      }
+    },
+    /**
+     * 打开知识图谱
+     */
+    openMindMap() {
+      MangerGetBookMindMap({ book_id: this.projectId }).then(({ content }) => {
+        if (content) {
+          this.mindMapJsonData = JSON.parse(content);
+          this.isChildDataLoad = true;
+        }
+      });
+      this.visibleMindMap = true;
+    },
+
+    async handleNodeClick(data) {
+      let [nodeId, componentId] = data.split('#');
+      if (nodeId) this.selectNode(nodeId);
+      if (componentId) {
+        let node = await this.$refs.courserware.findChildComponentByKey(componentId);
+        if (node) {
+          await this.$nextTick();
+          this.$refs.previewMain.scrollTo({
+            top: node.$el.offsetTop - 50,
+            left: node.$el.offsetLeft - 50,
+            behavior: 'smooth',
+          });
+        }
+      }
+      this.visibleMindMap = false;
+      this.visibleVisNetwork = false;
+    },
+
+    async openVisNetwork() {
+      this.visibleVisNetwork = true;
+    },
+
+    // 计算抽屉滑出位置
+    calcDrawerPosition() {
+      const menu = this.$refs.sidebarMenu;
+      if (menu) {
+        const rect = menu.getBoundingClientRect();
+        this.drawerStyle = {
+          top: `${rect.top}px`,
+          height: `${rect.height}px`,
+          left: `${rect.right - 240}px`,
+        };
+      }
+    },
+    /**
+     * 打开抽屉并初始化加载
+     * @param {Object} param - 抽屉参数
+     * @param {string} param.type - 抽屉类型(0: 图片, 1: 音频, 2: 视频)
+     */
+    openDrawer({ type }) {
+      if (this.drawerType === type) {
+        this.drawerType = '';
+        return;
+      }
+      // 重置所有加载状态
+      this.activeBookChapterId = '';
+      this.resetLoadState();
+      this.drawerType = type;
+      this.$nextTick(() => {
+        // 确保DOM更新后触发加载
+        // this.loadMore();
+        this.loadBookChapterStructExpandList();
+      });
+    },
+    openAudit() {
+      this.drawerType = '';
+    },
+    resetLoadState() {
+      this.cur_page = 1;
+      this.file_list = [];
+      this.total_count = 0;
+      this.loading = false;
+      this.lastLoadTime = 0; // 重置时间戳,允许立即加载
+      this.loadCount = 0;
+    },
+    /**
+     * 加载章节
+     */
+    async loadBookChapterStructExpandList() {
+      const params = {
+        book_id: this.projectId,
+        node_deep_mode: 3, // 节点深度模式 0【全部】,1【只查询章节】2【只查询非叶子章节】3【只查询第一层】
+        is_contain_root_node: 'false', // 是否包含根节点(把教材作为根节点)
+        is_contain_producer: 'false', // 是否包含制作人信息
+        is_contain_auditor: 'false', // 是否包含审核人信息
+      };
+      await GetBookChapterStructExpandList(params).then(({ node_list }) => {
+        this.bookChapterList = node_list || [];
+      });
+    },
+    /**
+     * 加载章节下的资源
+     */
+    async loadmultimediaList() {
+      const params = {
+        page_capacity: this.page_capacity,
+        cur_page: this.cur_page,
+        book_id: this.projectId,
+        book_chapter_node_id: this.activeBookChapterId,
+        type: parseInt(this.drawerType),
+      };
+      await PageQueryBookResourceList(params)
+        .then(({ total_count, resource_list }) => {
+          this.total_count = total_count;
+          this.file_list = resource_list || [];
+        })
+        .finally(() => {
+          this.loading = false;
+        });
+    },
+    /**
+     * 点击章节,切换数据
+     */
+    async multimediaHandleChange() {
+      const item = this.bookChapterList.find((item) => item.id === this.activeBookChapterId);
+      if (item) {
+        this.multimediaLoadingStates = true;
+        if (!item.data && !item.error) {
+          await this.loadmultimediaList();
+          let tmpList = this.file_list && this.file_list.length > 0 ? [...this.file_list] : null;
+          this.$set(item, 'data', tmpList);
+          this.multimediaLoadingStates = false;
+        } else {
+          this.multimediaLoadingStates = false;
+        }
+      }
+    },
+
+    // 加载更多数据
+    async loadMore() {
+      const now = Date.now();
+      // 只有当lastLoadTime不为0(不是第一次)且时间间隔太短时才return
+      if (this.lastLoadTime > 0 && now - this.lastLoadTime < this.minLoadInterval) {
+        return;
+      }
+
+      if (this.disabled || this.loading) {
+        if (this.lastLoadTime > 0) {
+          return;
+        }
+      }
+      this.loading = true;
+      const params = {
+        page_capacity: this.page_capacity,
+        cur_page: this.cur_page,
+        book_id: this.projectId,
+        book_chapter_node_id: this.activeBookChapterId,
+        type: parseInt(this.drawerType),
+      };
+      await PageQueryBookResourceList(params)
+        .then(({ total_count, resource_list }) => {
+          this.total_count = total_count;
+          // 记录加载前的滚动高度
+          const scrollContainer = this.$el.querySelector('.scroll-container');
+          const isAtBottom = this.isScrollAtBottom(scrollContainer);
+
+          this.file_list = this.cur_page === 1 ? resource_list : [...this.file_list, ...resource_list];
+          if (!resource_list || resource_list.length === 0) {
+            return;
+          }
+          this.cur_page += 1;
+
+          // 只有当前已经在底部时才微调滚动位置
+          if (isAtBottom) {
+            this.$nextTick(() => {
+              // 轻微向上滚动,创造滚动空间
+              scrollContainer.scrollTop -= 5;
+            });
+          }
+        })
+        .finally(() => {
+          this.loading = false;
+          this.lastLoadTime = now;
+        });
+    },
+    isScrollAtBottom(container) {
+      if (!container) return false;
+      return container.scrollHeight - container.scrollTop <= container.clientHeight + 5;
+    },
+    async handleFileClick(courseware_id, component_id) {
+      if (courseware_id) this.selectNode(courseware_id);
+      if (component_id) {
+        let node = await this.$refs.courserware.findChildComponentByKey(component_id);
+        if (node) {
+          await this.$nextTick();
+          this.$refs.previewMain.scrollTo({
+            top: node.offsetTop - 50,
+            left: node.offsetLeft - 50,
+            behavior: 'smooth',
+          });
+        }
+      }
+    },
+
+    /**
+     * 文本转换
+     * @param {string} text - 要转换的文本
+     * @returns {string} - 转换后的文本
+     */
+    convertText(text) {
+      if (this.chinese === 'zh-Hant' && this.opencc) {
+        try {
+          if (/<[a-z][\s\S]*>/i.test(text)) {
+            return this.convertHtmlPreserveAttributes(text);
+          }
+        } catch (e) {
+          return text;
+        }
+        return this.opencc(text);
+      }
+      return text;
+    },
+
+    /**
+     * 使用OpenCC解析HTML并仅转换文本节点,保留属性(包括内联样式/font-family)保持不变。防止字体名称像“微软雅黑' 正在转换为'微軟雅黑'.
+     */
+    convertHtmlPreserveAttributes(html) {
+      try {
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(html, 'text/html');
+
+        // 跳过这些标记内的转换
+        const skipTags = new Set(['SCRIPT', 'STYLE']);
+
+        const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, null, false);
+        let node = walker.nextNode();
+        while (node) {
+          const parent = node.parentNode;
+          if (parent && !skipTags.has(parent.nodeName)) {
+            const v = node.nodeValue;
+            if (v && v.trim()) {
+              node.nodeValue = this.opencc(v);
+            }
+          }
+          node = walker.nextNode();
+        }
+
+        return doc.body.innerHTML;
+      } catch (err) {
+        try {
+          return this.opencc(html);
+        } catch (e) {
+          return html;
+        }
+      }
+    },
+
+    simulateAnswer(disabled = true) {
+      this.$refs.courserware.simulateAnswer(this.isJudgeCorrect, this.isShowAnswer, disabled);
+    },
+
+    /**
+     * 选择节点
+     * @param {string} nodeId - 节点ID
+     * @param {boolean} isLeaf - 是否是叶子节点
+     */
+    selectChapterNode(nodeId, isLeaf) {
+      if (!isLeaf) return;
+      if (this.curSelectId === nodeId) return;
+      this.curSelectId = nodeId;
+      this.selectNode(nodeId);
+    },
+    /**
+     * 计算章节名称样式
+     * @param {number} deep - 节点深度
+     * @param {boolean} isLeaf - 是否是叶子节点
+     * @returns {Object} - 样式对象
+     */
+    computedNameStyle(deep, isLeaf) {
+      return {
+        'padding-left': `${(deep - 1) * 8}px`,
+        cursor: isLeaf ? 'pointer' : 'auto',
+      };
+    },
+    /**
+     * 切换右侧工具栏显示与隐藏
+     */
+    toggleSidebarShow() {
+      this.sidebarShow = !this.sidebarShow;
+    },
+
+    backTop() {
+      this.$refs.previewMain.scrollTo({
+        top: 0,
+        left: 0,
+        behavior: 'smooth',
+      });
+    },
+
+    /**
+     * 定位到对应位置
+     * @param {Object} item - 位置对象
+     * @param {number} type - 定位类型(1: 笔记定位, 2: 收藏定位, 3: 资源定位)
+     */
+    handleLocation(item, type) {
+      if (type === 3) {
+        let did = `${item.courseware_id}#${item.component_id}`;
+        this.handleNodeClick(did);
+        this.curSelectId = item.courseware_id;
+        return;
+      }
+
+      if (this.$refs.courserware && this.$refs.courserware.handleLocation) {
+        item.type = type;
+        this.$refs.courserware.handleLocation(item);
+      }
+    },
+    async getNote(params) {
+      if (params && params.type) this.drawerType = Number(params.type);
+      this.allNoteList = [];
+      await GetMyNoteList({ courseware_id: this.curSelectId }).then((res) => {
+        if (res.status === 1) {
+          res.note_list.forEach((x) => {
+            if (x.note_desc) {
+              let n = JSON.parse(x.note_desc);
+              let obj = {
+                coursewareId: x.courseware_id,
+                id: x.id,
+                blockId: n.blockId,
+                startIndex: n.startIndex,
+                endIndex: n.endIndex,
+                text: n.text,
+                note: n.note,
+              };
+              this.allNoteList.push(obj);
+            }
+          });
+        }
+      });
+    },
+    async handEditNote(note) {
+      this.oldRichData = {};
+      if (this.allNoteList.length === 0) {
+        await this.getNote();
+      }
+      let old = this.allNoteList.find(
+        (x) =>
+          x.coursewareId === note.coursewareId &&
+          x.blockId === note.blockId &&
+          x.startIndex === note.startIndex &&
+          x.endIndex === note.endIndex,
+      );
+      if (old) {
+        this.oldRichData = old;
+      }
+      this.newSelectedInfo = note;
+      this.editDialogOpen = true;
+    },
+    saveNote(note) {
+      let noteInfo = {
+        blockId: this.newSelectedInfo.blockId,
+        startIndex: this.newSelectedInfo.startIndex,
+        endIndex: this.newSelectedInfo.endIndex,
+        note: note.note,
+        text: this.newSelectedInfo.text,
+      };
+
+      let reqData = {
+        courseware_id: this.newSelectedInfo.coursewareId, // 课件 ID
+        component_id: 'WHOLE',
+        note_desc: JSON.stringify(noteInfo), // 位置描述
+      };
+      if (note.id) {
+        if (!noteInfo.note) {
+          this.delNote(note.id);
+          return;
+        }
+        let upDate = {
+          id: note.id,
+          note_desc: reqData.note_desc,
+        };
+        UpdateMyNote(upDate).then(() => {
+          this.getNote();
+        });
+      } else {
+        AddMyNote(reqData).then(() => {
+          this.getNote();
+        });
+      }
+      this.editDialogOpen = false;
+      this.newSelectedInfo = null;
+      this.selectedInfo = null;
+    },
+    handDelNote(id) {
+      this.$confirm('确定要删除此条笔记吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          this.delNote(id);
+        })
+        .catch(() => {});
+    },
+    delNote(id) {
+      const noteId = id || (this.oldRichData && this.oldRichData.id);
+      if (!noteId) return;
+      DeleteMyNote({ id: noteId }).then(() => {
+        this.allNoteList = this.allNoteList.filter((x) => x.id !== noteId);
+      });
+    },
+
+    async getCollect(params) {
+      if (params && params.type) this.drawerType = Number(params.type);
+      this.allCottectList = [];
+      await GetMyCollectList({ courseware_id: this.curSelectId }).then((res) => {
+        if (res.status === 1) {
+          res.collect_list.forEach((x) => {
+            if (x.collect_desc) {
+              let n = JSON.parse(x.collect_desc);
+              let obj = {
+                coursewareId: x.courseware_id,
+                id: x.id,
+                blockId: n.blockId,
+                startIndex: n.startIndex,
+                endIndex: n.endIndex,
+                text: n.text,
+              };
+              this.allCottectList.push(obj);
+            }
+          });
+        }
+      });
+    },
+    async saveCollect(collect) {
+      if (this.allCottectList.length === 0) {
+        await this.getCollect();
+      }
+
+      let old = this.allCottectList.find(
+        (x) =>
+          x.coursewareId === collect.coursewareId &&
+          x.blockId === collect.blockId &&
+          x.startIndex === collect.startIndex &&
+          x.endIndex === collect.endIndex,
+      );
+      if (old) {
+        this.$message({
+          dangerouslyUseHTMLString: true,
+          message: "<i class='el-icon-check' />已收藏",
+        });
+        return;
+      }
+      this.newSelectedInfo = collect;
+      let collectInfo = {
+        blockId: this.newSelectedInfo.blockId,
+        startIndex: this.newSelectedInfo.startIndex,
+        endIndex: this.newSelectedInfo.endIndex,
+        text: this.newSelectedInfo.text,
+      };
+
+      let reqData = {
+        courseware_id: this.newSelectedInfo.coursewareId, // 课件 ID
+        component_id: 'WHOLE',
+        collect_desc: JSON.stringify(collectInfo), // 位置描述
+      };
+      AddMyCollect(reqData)
+        .then(() => {
+          this.getCollect();
+        })
+        .then(() => {
+          this.$message({
+            dangerouslyUseHTMLString: true,
+            message: "<i class='el-icon-check' />已收藏",
+          });
+        });
+    },
+    handDelCollect(id) {
+      this.$confirm('确定要删除此条收藏吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          DeleteMyCollect({ id }).then(() => {
+            this.allCottectList = this.allCottectList.filter((x) => x.id !== id);
+          });
+        })
+        .catch(() => {});
+    },
+    getSearch(params) {
+      if (params && params.type) this.drawerType = Number(params.type);
+    },
+    async querySearchList() {
+      this.searchList = [];
+      if (!this.searchContent) return;
+      await SearchBookContentText({ book_id: this.projectId, text: this.searchContent }).then((res) => {
+        if (res.status === 1) {
+          this.searchList = res.courseware_component_list;
+        }
+      });
+    },
+    fullScreen() {
+      toggleFullScreen(this.$refs.previewMain);
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+$total-width: $courseware-width + $courseware-left-margin + $courseware-right-margin;
+
+.common-preview {
+  &__header {
+    position: sticky;
+    top: 0;
+    left: 0;
+    z-index: 9;
+    display: flex;
+    align-items: center;
+    height: 40px;
+    padding: 6px 4px;
+    margin-bottom: 5px;
+    background-color: #fff;
+    border-top: $border;
+    border-bottom: $border;
+
+    .menu-container {
+      display: flex;
+      justify-content: space-between;
+      width: 240px;
+      padding: 4px 8px;
+      border-right: $border;
+    }
+
+    .courseware {
+      display: flex;
+      flex-grow: 1;
+      column-gap: 16px;
+      align-items: center;
+      justify-content: space-between;
+      height: 40px;
+      padding: 0 8px;
+
+      .lang-select {
+        :deep .el-input {
+          width: 100px;
+        }
+
+        :deep .el-input__inner {
+          height: 24px;
+          line-height: 24px;
+          background-color: #fff;
+        }
+
+        :deep .el-input__icon {
+          line-height: 24px;
+        }
+      }
+
+      .permission-control {
+        .student {
+          margin-right: 12px;
+        }
+      }
+
+      .operator {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+
+        .link {
+          + .link {
+            margin-left: 0;
+
+            &::before {
+              margin-right: 8px;
+              color: #999;
+              content: '|';
+            }
+          }
+        }
+      }
+    }
+  }
+
+  .main-container {
+    position: relative;
+    flex: 1;
+    min-width: 1110px;
+    padding: 15px 0;
+    overflow: auto;
+    background-color: #ececec;
+
+    .catalogue-bar {
+      position: absolute;
+      top: 15px;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 54px;
+      height: 54px;
+      margin: -9px 6px 0 240px;
+      cursor: pointer;
+      background-color: #fff;
+      border-radius: 2px;
+      box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 40%);
+    }
+
+    .sidebar-bar {
+      position: absolute;
+      top: 0;
+      right: 240px;
+      display: flex;
+      width: 60px;
+      height: calc(100vh - 166px);
+
+      .toolbar {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 60px;
+        height: 100%;
+
+        img {
+          cursor: pointer;
+        }
+
+        &-special {
+          display: flex;
+          flex-direction: column;
+          row-gap: 16px;
+          align-items: center;
+          width: 100%;
+          margin-bottom: 24px;
+          background-color: #fff;
+
+          img {
+            width: 36px;
+            height: 36px;
+          }
+        }
+      }
+    }
+  }
+
+  .back-top {
+    position: absolute;
+    right: 240px;
+    bottom: 0;
+    display: flex;
+    place-content: center center;
+    align-items: center;
+    width: 60px;
+    height: 60px;
+    cursor: pointer;
+    background-color: #fff;
+  }
+
+  main.preview-main {
+    display: flex;
+    flex: 1;
+    width: calc($total-width);
+    min-width: calc($total-width);
+    max-width: calc($total-width);
+    min-height: 100%;
+    margin: 0 auto;
+    background-color: #fff;
+    border-radius: 4px;
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 40%);
+
+    .preview-left {
+      width: $courseware-left-margin;
+      min-width: $courseware-left-margin;
+      max-width: $courseware-left-margin;
+      background-color: $courseware-bgColor;
+    }
+
+    .preview-right {
+      width: $courseware-right-margin;
+      min-width: $courseware-right-margin;
+      max-width: $courseware-right-margin;
+      background-color: $courseware-bgColor;
+    }
+
+    &.no-audit {
+      margin: 0 auto;
+    }
+  }
+
+  .audit-content {
+    display: flex;
+    min-width: 1810px;
+    height: calc(100vh - 46px);
+
+    .left-menu {
+      display: flex;
+      flex-direction: column;
+      width: $catalogue-width;
+      font-family: 'Microsoft YaHei', 'Arial', sans-serif;
+      background-color: #fff;
+
+      .courseware-info {
+        display: flex;
+        column-gap: 18px;
+        width: 100%;
+        height: 186px;
+        padding: 6px 6px 24px;
+        border-bottom: $border;
+
+        .cover-image {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 111px;
+          height: 157px;
+          background-color: rgba(229, 229, 229, 100%);
+
+          img {
+            max-width: 111px;
+            max-height: 157px;
+          }
+        }
+
+        .info-content {
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+
+          .catalogue-icon {
+            text-align: right;
+
+            .svg-icon {
+              cursor: pointer;
+            }
+          }
+
+          .courseware {
+            width: 159px;
+            height: 64px;
+            font-size: 16px;
+
+            .name {
+              font-weight: bold;
+            }
+
+            .editor {
+              display: -webkit-box;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              word-break: break-word;
+              white-space: normal;
+              -webkit-line-clamp: 2; /* 多行省略行数,按需调整 */
+              -webkit-box-orient: vertical;
+            }
+          }
+        }
+      }
+
+      .courseware-tree {
+        display: flex;
+        flex: 1;
+        flex-direction: column;
+        row-gap: 8px;
+        padding: 12px;
+        margin-top: 12px;
+        overflow: auto;
+
+        .menu-item {
+          display: flex;
+          align-items: center;
+
+          &:not(.courseware) {
+            font-weight: bold;
+          }
+
+          &.courseware {
+            &:hover {
+              .name {
+                background-color: #f3f3f3;
+              }
+            }
+          }
+
+          .svg-icon {
+            margin-left: 4px;
+
+            &.my-edit-task {
+              color: $right-color;
+            }
+          }
+
+          .name {
+            flex: 1;
+            padding: 4px 8px 4px 4px;
+            border-radius: 4px;
+          }
+
+          &.active {
+            .name {
+              font-weight: bold;
+              color: #4095e5;
+            }
+          }
+        }
+      }
+    }
+
+    .sidebar {
+      position: relative;
+      display: flex;
+      width: $sidebar-width;
+
+      .toolbar {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 60px;
+        height: 100%;
+        background-color: rgba(247, 248, 250, 100%);
+
+        img {
+          cursor: pointer;
+        }
+
+        &-special {
+          display: flex;
+          flex-direction: column;
+          row-gap: 16px;
+          margin-bottom: 24px;
+        }
+
+        &-list {
+          display: flex;
+          flex-direction: column;
+          row-gap: 16px;
+          align-items: center;
+          width: 100%;
+
+          .sidebar-item {
+            width: 100%;
+            padding-top: 2px;
+            text-align: center;
+
+            .sidebar-icon {
+              width: 36px;
+              height: 36px;
+              cursor: pointer;
+            }
+
+            &.active {
+              background-color: #4095e5;
+            }
+          }
+        }
+      }
+
+      .content {
+        flex: 1;
+        background-color: #fff;
+
+        .resource_box {
+          height: 100%;
+          overflow-y: auto;
+          border: 1px solid #e5e5e5;
+
+          .source-toolbar-list {
+            position: fixed;
+            z-index: 999;
+            display: flex;
+            width: 240px;
+            background: #f2f3f5;
+
+            .sidebar-item {
+              width: 100%;
+              padding-top: 5px;
+              text-align: center;
+              border-right: 1px solid #ccc;
+
+              .sidebar-icon {
+                width: 26px;
+                height: 26px;
+                cursor: pointer;
+              }
+
+              &.active {
+                background-color: #4095e5;
+              }
+            }
+          }
+
+          h5 {
+            position: fixed;
+            z-index: 999;
+            width: 240px;
+            padding: 0 5px;
+            margin: 0;
+            font-size: 18px;
+            line-height: 40px;
+            background: #f2f3f5;
+          }
+
+          .scroll-container {
+            display: flex;
+            flex-direction: column;
+            row-gap: 8px;
+            margin: 6px;
+
+            .list-item {
+              // display: flex;
+              align-items: center;
+              cursor: pointer;
+              border: 1px solid #ccc;
+              border-radius: 8px;
+
+              :deep .el-slider {
+                .el-slider__runway {
+                  background-color: #eee;
+                }
+              }
+
+              .el-image {
+                display: flex;
+                width: 100%;
+                min-width: 100%;
+                height: 90px;
+                background-color: #ccc;
+                border-radius: 8px;
+              }
+
+              .video-play {
+                width: 100%;
+                min-width: 100%;
+              }
+
+              .text-box {
+                word-break: break-word;
+              }
+            }
+          }
+
+          p {
+            color: #999;
+            text-align: center;
+          }
+
+          .card-box li {
+            padding: 10px;
+            border-bottom: 1px solid #ccc;
+
+            .el-icon-notebook-2 {
+              display: block;
+              margin-bottom: 4px;
+              font-size: 12px;
+              color: grey;
+            }
+          }
+
+          .mark {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 3px;
+
+            .word {
+              flex: 1;
+              margin-right: 10px;
+              word-break: break-all;
+            }
+
+            .linkLocation {
+              cursor: pointer;
+            }
+          }
+
+          ::v-deep .el-collapse-item__header {
+            margin-left: 5px;
+          }
+
+          .loading-text {
+            padding: 5px;
+            color: #999;
+            text-align: center;
+          }
+
+          .error-text {
+            padding: 5px;
+            color: #999;
+            text-align: center;
+          }
+        }
+      }
+
+      .back-top {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        display: flex;
+        place-content: center center;
+        align-items: center;
+        width: 60px;
+        height: 60px;
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+:deep .scroll-container .audio-wrapper {
+  width: 100% !important;
+
+  .audio-middle {
+    width: 100% !important;
+    padding: 6px 8px !important;
+    margin-left: 0;
+    border: none;
+    border-radius: 8px;
+
+    .audio-name {
+      text-align: left;
+    }
+
+    .slider-area {
+      column-gap: 8px !important;
+    }
+
+    :deep .remark-dialog {
+      .el-dialog__body {
+        padding: 5px 20px;
+      }
+    }
+
+    :deep .audit-dialog {
+      .el-dialog__body {
+        height: calc(100vh - 260px);
+        padding: 5px 20px;
+      }
+
+      .mind-map-container .mind-map {
+        height: calc(100vh - 310px);
+      }
+    }
+  }
+}
+
+.mt10 {
+  margin: 10px 0 0 !important;
+  background-color: #eee;
+}
+</style>
+
+<style lang="scss">
+.tox-tinymce-aux {
+  z-index: 9999 !important;
+}
+</style>

+ 6 - 2
src/enterPage.vue

@@ -39,6 +39,12 @@ export default {
           if (this.isMobile()) {
             this.$router.replace({ name: 'MobileBookPreview' });
           } else {
+            if (this.data.preview_type === 1) {
+              setLocalStore('task_id', this.data.task_id); // 课程预览需要 task_id
+              setLocalStore('courseware_id', this.data.courseware_id);
+              this.$router.replace({ name: 'CoursewarePreview' });
+              return;
+            }
             this.$router.replace({ name: 'BookPreview' });
           }
         })
@@ -52,5 +58,3 @@ export default {
   },
 };
 </script>
-
-<style lang="scss" scoped></style>

+ 8 - 0
src/router/modules/basic.js

@@ -16,6 +16,12 @@ export const BookPreview = {
   component: () => import('@/web_preview/index.vue'),
 };
 
+export const CoursewarePreview = {
+  path: '/courseware_preview',
+  name: 'CoursewarePreview',
+  component: () => import('@/courseware_preview/index.vue'),
+};
+
 export const MobileBookPreview = {
   path: '/mobile_book_preview',
   name: 'MobileBookPreview',
@@ -27,3 +33,5 @@ export const NotFoundPage = {
   name: '404',
   component: () => import('@/views/not_found/404.vue'),
 };
+
+export default [EnterPage, OBOCPage, BookPreview, CoursewarePreview, MobileBookPreview, NotFoundPage];

+ 2 - 2
src/router/modules/index.js

@@ -1,3 +1,3 @@
-import { BookPreview, MobileBookPreview, EnterPage, OBOCPage, NotFoundPage } from './basic';
+import basicRoutes from './basic';
 
-export const routes = [BookPreview, MobileBookPreview, EnterPage, OBOCPage, NotFoundPage];
+export const routes = basicRoutes;

+ 66 - 0
src/utils/directive.js

@@ -11,3 +11,69 @@ Vue.directive('numericOnly', {
     });
   },
 });
+
+/**
+ * el-dialog 组件拖拽指令
+ */
+Vue.directive('dialogDrag', {
+  bind(el) {
+    const dialog = el.querySelector('.el-dialog');
+    const header = el.querySelector('.el-dialog__header');
+    if (!dialog || !header) return;
+
+    header.style.cursor = 'move';
+
+    const resetPosition = () => {
+      dialog.style.left = '';
+      dialog.style.top = '';
+      dialog.style.position = '';
+      dialog.style.margin = '';
+    };
+
+    // 当对话框隐藏时重置其位置
+    const observer = new MutationObserver(() => {
+      const isHidden = window.getComputedStyle(el).display === 'none';
+      if (isHidden) resetPosition();
+    });
+
+    // 监听 el 的 style 和 class 属性变化,以检测对话框的显示状态
+    observer.observe(el, {
+      attributes: true,
+      attributeFilter: ['style'],
+    });
+
+    el.__dialogDragCleanup__ = () => observer.disconnect();
+
+    header.onmousedown = (e) => {
+      const startX = e.clientX;
+      const startY = e.clientY;
+      const rect = dialog.getBoundingClientRect();
+      const startLeft = rect.left;
+      const startTop = rect.top;
+
+      const onMouseMove = (moveEvent) => {
+        const dx = moveEvent.clientX - startX;
+        const dy = moveEvent.clientY - startY;
+        dialog.style.margin = 0;
+        dialog.style.left = `${startLeft + dx}px`;
+        dialog.style.top = `${startTop + dy}px`;
+        dialog.style.position = 'fixed';
+      };
+
+      const onMouseUp = () => {
+        document.removeEventListener('mousemove', onMouseMove);
+        document.removeEventListener('mouseup', onMouseUp);
+      };
+
+      document.addEventListener('mousemove', onMouseMove);
+      document.addEventListener('mouseup', onMouseUp);
+    };
+  },
+  unbind(el) {
+    // 清理事件监听器和观察者
+    if (el.__dialogDragCleanup__) {
+      el.__dialogDragCleanup__();
+      delete el.__dialogDragCleanup__;
+    }
+  },
+});

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

@@ -0,0 +1,269 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <el-dialog
+    v-dialogDrag
+    custom-class="answer-analysis-dialog"
+    :visible="visible"
+    width="65vw"
+    :close-on-click-modal="false"
+    :before-close="handleClose"
+  >
+    <!-- 正确答案 -->
+    <div v-if="hasRightAnswerSlot" class="right-answer">
+      <div class="right-answer-title">
+        <span class="right-answer-title-icon"></span>
+        <span class="right-answer-title-image"></span>
+      </div>
+      <slot name="right-answer"></slot>
+    </div>
+
+    <!-- 参考答案 -->
+    <template v-if="answerList.length > 0">
+      <div v-for="(item, i) in answerList" :key="`answer-${i}`" class="answer-list">
+        <div class="answer">
+          <div class="answer-title">
+            <span class="answer-title-icon"></span>
+            <span class="answer-title-image"></span>
+          </div>
+          <div class="rich-text" v-html="item.answer_rich_text"></div>
+          <div v-if="item.answer_audio_list.length > 0" class="answer-audio-list">
+            <AudioPlay
+              v-for="file in item.answer_audio_list"
+              :key="file.file_id"
+              :file-id="file.file_id"
+              :file-name="file.file_name.slice(0, file.file_name.lastIndexOf('.'))"
+              :audio-index="i"
+            />
+          </div>
+          <div v-if="item.answer_image_list.length > 0" class="answer-image-list">
+            <img
+              v-for="file in item.answer_image_list"
+              :key="file.file_id"
+              :src="file.file_url"
+              :alt="file.file_name"
+            />
+          </div>
+          <div v-if="item.answer_video_list.length > 0" class="answer-video-list">
+            <VideoPlay
+              v-for="(file, j) in item.answer_video_list"
+              :key="file.file_id"
+              view-size="small"
+              :file-id="file.file_id"
+              :cur-video-index="j"
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+
+    <!-- 解析 -->
+    <template v-if="analysisList.length > 0">
+      <div v-for="(item, i) in analysisList" :key="`analysis-${i}`" class="analysis-list">
+        <div class="analysis">
+          <div class="analysis-title">
+            <span class="analysis-title-icon"></span>
+            <span class="analysis-title-image"></span>
+          </div>
+          <div class="rich-text" v-html="item.analysis_rich_text"></div>
+          <div v-if="item.analysis_audio_list.length > 0" class="analysis-audio-list">
+            <AudioPlay
+              v-for="file in item.analysis_audio_list"
+              :key="file.file_id"
+              :file-id="file.file_id"
+              :file-name="file.file_name.slice(0, file.file_name.lastIndexOf('.'))"
+              :audio-index="i"
+            />
+          </div>
+          <div v-if="item.analysis_image_list.length > 0" class="analysis-image-list">
+            <img
+              v-for="file in item.analysis_image_list"
+              :key="file.file_id"
+              :src="file.file_url"
+              :alt="file.file_name"
+            />
+          </div>
+          <div v-if="item.analysis_video_list.length > 0" class="analysis-video-list">
+            <VideoPlay
+              v-for="(file, j) in item.analysis_video_list"
+              :key="file.file_id"
+              view-size="small"
+              :file-id="file.file_id"
+              :cur-video-index="j"
+            />
+          </div>
+        </div>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import AudioPlay from '@/views/book/courseware/preview/components/common/AudioPlay.vue';
+import VideoPlay from '@/views/book/courseware/preview/components/common/VideoPlay.vue';
+
+export default {
+  name: 'AnswerAnalysis',
+  components: { AudioPlay, VideoPlay },
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    answerList: {
+      type: Array,
+      required: true,
+      default: () => [],
+    },
+    analysisList: {
+      type: Array,
+      required: true,
+      default: () => [],
+    },
+  },
+  data() {
+    return {};
+  },
+  computed: {
+    hasRightAnswerSlot() {
+      const slotContent = this.$slots['right-answer']; // 获取名为 'right-answer' 的插槽内容
+      return Array.isArray(slotContent) && slotContent.length > 0; // 检查插槽内容是否存在且不为空
+    },
+  },
+  methods: {
+    handleClose() {
+      this.$emit('update:visible', false);
+      this.$emit('closeAnswerAnalysis');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.right-answer {
+  margin-bottom: 16px;
+
+  .right-answer-title {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+    margin-bottom: 8px;
+
+    &-icon {
+      width: 32px;
+      height: 32px;
+      background: url('@/assets/component/correct-answer-icon.png') no-repeat center/contain;
+    }
+
+    &-image {
+      width: 181px;
+      height: 32px;
+      background: url('@/assets/component/correct-answer.png') no-repeat center/contain;
+    }
+  }
+}
+
+.answer-list {
+  width: 100%;
+
+  .answer {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+
+    &-title {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+      margin-bottom: 8px;
+
+      &-icon {
+        width: 32px;
+        height: 32px;
+        background: url('@/assets/component/reference-answer-icon.png') no-repeat center/contain;
+      }
+
+      &-image {
+        width: 211px;
+        height: 32px;
+        background: url('@/assets/component/reference-answer.png') no-repeat center/contain;
+      }
+    }
+
+    .answer-image-list {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+    }
+  }
+
+  & + .analysis-list {
+    padding-top: 24px;
+    margin-top: 24px;
+    border-top: $border;
+  }
+}
+
+.analysis-list {
+  width: 100%;
+
+  .analysis {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+    margin-bottom: 16px;
+
+    strong {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+
+    .analysis {
+      &-title {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+        margin-bottom: 8px;
+
+        &-icon {
+          width: 16px;
+          height: 16px;
+          background: url('@/assets/component/answer-analysis-icon.png') no-repeat center/contain;
+        }
+
+        &-image {
+          width: 191px;
+          height: 32px;
+          background: url('@/assets/component/answer-analysis.png') no-repeat center/contain;
+        }
+      }
+
+      .analysis-image-list {
+        display: flex;
+        flex-wrap: wrap;
+        gap: 8px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.answer-analysis-dialog {
+  .el-dialog__body {
+    max-height: 75vh;
+    overflow: auto;
+  }
+}
+</style>

+ 66 - 0
src/views/book/courseware/preview/common/PreviewOperation.vue

@@ -0,0 +1,66 @@
+<template>
+  <div class="operation">
+    <div v-show="permissionControl.can_answer" class="button retry" @click="retry()"></div>
+    <div v-show="permissionControl.can_correct || permissionControl.can_check_correct" class="button correct"></div>
+    <div
+      v-show="permissionControl.can_judge_correct || permissionControl.can_show_answer"
+      class="button answer"
+      @click="showAnswerAnalysis()"
+    ></div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PreviewOperation',
+  inject: ['getPermissionControl'],
+  data() {
+    return {};
+  },
+  computed: {
+    permissionControl() {
+      return this.getPermissionControl();
+    },
+  },
+  methods: {
+    showAnswerAnalysis() {
+      this.$emit('showAnswerAnalysis');
+    },
+    // 重做
+    retry() {
+      this.$emit('retry');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.operation {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 8px;
+
+  .button {
+    width: 90px;
+    height: 40px;
+    cursor: pointer;
+    border-radius: 5px;
+
+    & + .button {
+      margin-left: 24px;
+    }
+
+    &.retry {
+      background: url('@/assets/component/component-retry.png') no-repeat center;
+    }
+
+    &.correct {
+      background: url('@/assets/component/component-correct.png') no-repeat center;
+    }
+
+    &.answer {
+      background: url('@/assets/component/component-answer.png') no-repeat center;
+    }
+  }
+}
+</style>

+ 15 - 0
src/views/book/courseware/preview/components/character/CharacterPreview.vue

@@ -55,6 +55,10 @@
                       class="items-image"
                       :src="items.file_list[0].file_url"
                       fit="contain"
+                      :style="{
+                        borderColor:
+                          data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
+                      }"
                     />
                   </template>
                   <template v-else-if="items && items.type === 'lian'">
@@ -394,6 +398,13 @@
           </div>
         </div>
       </div>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+      <AnswerAnalysis
+        :visible.sync="visibleAnswerAnalysis"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        @closeAnswerAnalysis="closeAnswerAnalysis"
+      />
     </div>
   </div>
 </template>
@@ -870,5 +881,9 @@ export default {
     margin-top: 3px;
     word-break: break-word;
   }
+
+  .operation {
+    width: 100%;
+  }
 }
 </style>

+ 77 - 68
src/views/book/courseware/preview/components/character_structure/CharacterStructurePreview.vue

@@ -152,32 +152,79 @@
           </div>
         </div>
       </div>
-      <div v-if="isShowRightAnswer" class="right-answer">
-        <div class="title">{{ convertText('正确答案') }}</div>
-        <div class="one-box">
-          <div
-            v-for="(items, row) in data.option_list"
-            :key="'row' + row"
-            class="one"
-            :class="[!items.pinyin ? 'one_nopy' : '']"
-            :style="{ marginRight: (row + 1) % 3 == 0 ? '' : '16px' }"
-          >
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+      <AnswerAnalysis
+        :visible.sync="visibleAnswerAnalysis"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        @closeAnswerAnalysis="closeAnswerAnalysis"
+      >
+        <div slot="right-answer">
+          <div class="one-box">
             <div
-              class="number"
-              :style="{
-                background:
-                  data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '#346CDA',
-              }"
+              v-for="(items, row) in data.option_list"
+              :key="'row' + row"
+              class="one"
+              :class="[!items.pinyin ? 'one_nopy' : '']"
+              :style="{ marginRight: (row + 1) % 3 == 0 ? '' : '16px' }"
             >
-              {{ row + 1 }}
-            </div>
-            <div class="hzpinyin">
-              <div v-if="isEnable(data.property.view_pinyin)" class="pinyin">
-                {{ items.pinyin }}
+              <div
+                class="number"
+                :style="{
+                  background:
+                    data.unified_attrib && data.unified_attrib.topic_color
+                      ? data.unified_attrib.topic_color
+                      : '#346CDA',
+                }"
+              >
+                {{ row + 1 }}
+              </div>
+              <div class="hzpinyin">
+                <div v-if="isEnable(data.property.view_pinyin)" class="pinyin">
+                  {{ items.pinyin }}
+                </div>
+                <template v-if="items.hz_info.length > 0">
+                  <div
+                    class="strockplay-newWord"
+                    :style="{
+                      borderColor:
+                        data.unified_attrib && data.unified_attrib.topic_color
+                          ? data.unified_attrib.topic_color
+                          : '#346CDA',
+                    }"
+                  >
+                    <Strockplay
+                      class-name="adult-strockplay"
+                      :Book_text="items.hz_info[0].con"
+                      :play-storkes="true"
+                      :stroke-play-color="
+                        data.unified_attrib && data.unified_attrib.topic_color
+                          ? data.unified_attrib.topic_color
+                          : '#346CDA'
+                      "
+                      :stroke-color="'#000000'"
+                      :paly-width="'18px'"
+                      :BoxbgType="'0'"
+                      :cur-item="items.hz_info[0].hzDetail.hz_json"
+                      :target-div="'writeTops-item-right' + '-' + items.hz_info[0].con"
+                      class="writeTop-item"
+                      :style="{
+                        borderColor:
+                          data.unified_attrib && data.unified_attrib.topic_color
+                            ? data.unified_attrib.topic_color
+                            : '#346CDA',
+                      }"
+                    />
+                  </div>
+                </template>
+              </div>
+              <div class="image">
+                <img src="@/assets/drag-arrows.png" alt="" />
               </div>
-              <template v-if="items.hz_info.length > 0">
+              <div class="answer">
                 <div
-                  class="strockplay-newWord"
+                  class="option_one"
+                  :class="[items.is_example ? 'option_one_example' : '']"
                   :style="{
                     borderColor:
                       data.unified_attrib && data.unified_attrib.topic_color
@@ -185,59 +232,21 @@
                         : '#346CDA',
                   }"
                 >
-                  <Strockplay
-                    class-name="adult-strockplay"
-                    :Book_text="items.hz_info[0].con"
-                    :play-storkes="true"
-                    :stroke-play-color="
-                      data.unified_attrib && data.unified_attrib.topic_color
-                        ? data.unified_attrib.topic_color
-                        : '#346CDA'
+                  <img
+                    v-if="items.answer"
+                    :src="
+                      items.answer.length > 3
+                        ? data.file_list.find((p) => p.file_id === items.answer).file_url
+                        : require('@/assets/structure/structure-' + items.answer + '.png')
                     "
-                    :stroke-color="'#000000'"
-                    :paly-width="'18px'"
-                    :BoxbgType="'0'"
-                    :cur-item="items.hz_info[0].hzDetail.hz_json"
-                    :target-div="'writeTops-item-right' + '-' + items.hz_info[0].con"
-                    class="writeTop-item"
-                    :style="{
-                      borderColor:
-                        data.unified_attrib && data.unified_attrib.topic_color
-                          ? data.unified_attrib.topic_color
-                          : '#346CDA',
-                    }"
+                    alt=""
                   />
                 </div>
-              </template>
-            </div>
-            <div class="image">
-              <img src="@/assets/drag-arrows.png" alt="" />
-            </div>
-            <div class="answer">
-              <div
-                class="option_one"
-                :class="[items.is_example ? 'option_one_example' : '']"
-                :style="{
-                  borderColor:
-                    data.unified_attrib && data.unified_attrib.topic_color
-                      ? data.unified_attrib.topic_color
-                      : '#346CDA',
-                }"
-              >
-                <img
-                  v-if="items.answer"
-                  :src="
-                    items.answer.length > 3
-                      ? data.file_list.find((p) => p.file_id === items.answer).file_url
-                      : require('@/assets/structure/structure-' + items.answer + '.png')
-                  "
-                  alt=""
-                />
               </div>
             </div>
           </div>
         </div>
-      </div>
+      </AnswerAnalysis>
     </div>
   </div>
 </template>

+ 2 - 2
src/views/book/courseware/preview/components/common/AudioPlay.vue

@@ -13,7 +13,7 @@
             @change="changeCurrentTime"
           />
           <span class="audio-time">{{ audio_allTime }}</span>
-          <el-select v-model="audio.playbackRate" @change="onSpeedChange" style="width: 100px">
+          <el-select v-model="audio.playbackRate" style="width: 100px" @change="onSpeedChange">
             <el-option
               v-for="speed in playbackRateList.split(' ')"
               :key="speed"
@@ -34,7 +34,7 @@
       <div v-else class="independent-big">
         <div v-if="fileNameDisplay == 'true'" class="audio-name">{{ audioIndex + 1 }}. {{ fileName }}</div>
         <div class="slider-area">
-          <SvgIcon :icon-class="iconClass" size="18" :color="topicColor" @click="playAudio" style="cursor: pointer" />
+          <SvgIcon :icon-class="iconClass" size="18" :color="topicColor" style="cursor: pointer" @click="playAudio" />
           <span class="audio-time">{{ secondFormatConversion(audio.current_time) }}</span>
           <el-slider
             v-model="play_value"

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

@@ -1,5 +1,7 @@
 import SerialNumberPosition from './SerialNumberPosition.vue';
 import PinyinText from '@/components/PinyinText.vue';
+import AnswerAnalysis from '@/views/book/courseware/preview/common/AnswerAnalysis.vue';
+import PreviewOperation from '@/views/book/courseware/preview/common/PreviewOperation.vue';
 
 import { isEnable } from '@/views/book/courseware/data/common';
 import { ContentGetCoursewareComponentContent } from '@/api/book';
@@ -17,9 +19,11 @@ const mixin = {
       isShowParse: false, // 是否显示解析
       isEnable,
       loader: false,
+      visibleAnswerAnalysis: false, // 是否显示答案解析弹窗
+      answerAnalysisState: null, // 答案解析弹窗前的状态快照
     };
   },
-  inject: ['getLang', 'getChinese', 'convertText', 'getTitleList'],
+  inject: ['getLang', 'getChinese', 'convertText', 'getTitleList', 'getPermissionControl'],
   props: {
     id: {
       type: String,
@@ -46,6 +50,9 @@ const mixin = {
     showLang() {
       return this.getLang() !== 'ZH';
     },
+    permissionControl() {
+      return this.getPermissionControl();
+    },
   },
   watch: {
     content: {
@@ -60,6 +67,8 @@ const mixin = {
   components: {
     SerialNumberPosition,
     PinyinText,
+    AnswerAnalysis,
+    PreviewOperation,
   },
   created() {
     // 这里分为 预览 和 编辑调整位置、视频互动组件 三种情况
@@ -186,6 +195,30 @@ const mixin = {
         ...borderData,
       };
     },
+    showAnswerAnalysis() {
+      if (!this.answerAnalysisState) {
+        this.answerAnalysisState = {
+          disabled: this.disabled,
+          isJudgingRightWrong: this.isJudgingRightWrong,
+        };
+      }
+      this.visibleAnswerAnalysis = true;
+      this.disabled = true;
+      this.isJudgingRightWrong = this.permissionControl.can_judge_correct;
+      this.isShowRightAnswer = this.permissionControl.can_show_answer;
+    },
+    closeAnswerAnalysis() {
+      if (this.answerAnalysisState) {
+        this.disabled = this.answerAnalysisState.disabled;
+        this.isJudgingRightWrong = this.answerAnalysisState.isJudgingRightWrong;
+        this.isShowRightAnswer = false;
+        this.answerAnalysisState = null;
+      } else {
+        this.disabled = false;
+        this.isJudgingRightWrong = false;
+        this.isShowRightAnswer = false;
+      }
+    },
   },
 };
 

+ 102 - 4
src/views/book/courseware/preview/components/fill/FillPreview.vue

@@ -82,10 +82,6 @@
                   @handleWav="handleMiniWav($event, li.mark)"
                 />
               </template>
-
-              <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${j}`" class="right-answer">
-                {{ computedAnswerText(li.mark) }}
-              </span>
             </template>
           </template>
         </p>
@@ -106,6 +102,95 @@
     </div>
 
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" @retry="retry" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <div slot="right-answer" class="fill-wrapper">
+        <p v-for="(item, i) in modelEssay" :key="i">
+          <template v-for="(li, j) in item">
+            <template v-if="li.type === 'text'">
+              <PinyinText
+                v-if="isEnable(data.property.view_pinyin)"
+                :key="`${i}-${j}`"
+                class="content"
+                :paragraph-list="li.paragraph_list"
+                :pinyin-position="data.property.pinyin_position"
+                :is-preview="true"
+              />
+              <span v-else :key="j" v-html="convertText(sanitizeHTML(li.content))"></span>
+            </template>
+            <template v-if="li.type === 'input'">
+              <template v-if="data.property.fill_type === fillTypeList[0].value">
+                <el-input
+                  :key="j"
+                  v-model="li.content"
+                  :disabled="disabled"
+                  :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
+                  :style="[{ width: Math.max(80, li.content.length * 21.3) + 'px' }]"
+                />
+              </template>
+
+              <template v-else-if="data.property.fill_type === fillTypeList[1].value">
+                <el-popover :key="j" placement="top" trigger="click">
+                  <div class="word-list">
+                    <span
+                      v-for="{ content, mark } in data.word_list"
+                      :key="mark"
+                      class="word-item"
+                      @click="handleSelectWord(content, mark, li)"
+                    >
+                      {{ content }}
+                    </span>
+                  </div>
+
+                  <el-input
+                    slot="reference"
+                    v-model="li.content"
+                    :readonly="true"
+                    :class="[data.property.fill_font, ...computedAnswerClass(li.mark)]"
+                    class="pinyin"
+                    :style="[{ width: Math.max(80, li.content.length * 21.3) + 'px' }]"
+                  />
+                </el-popover>
+              </template>
+
+              <template v-else-if="data.property.fill_type === fillTypeList[2].value">
+                <span :key="j" class="write-click" @click="handleWriteClick(li.mark)">
+                  <img
+                    v-show="li.write_base64"
+                    style="background-color: #f4f4f4"
+                    :src="li.write_base64"
+                    alt="write-show"
+                  />
+                </span>
+              </template>
+
+              <template v-else-if="data.property.fill_type === fillTypeList[3].value">
+                <SoundRecordBox
+                  ref="record"
+                  :key="j"
+                  type="mini"
+                  :many-times="false"
+                  class="record-box"
+                  :attrib="data.unified_attrib"
+                  :answer-record-list="data.audio_answer_list"
+                  :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+                  @handleWav="handleMiniWav($event, li.mark)"
+                />
+              </template>
+
+              <span v-show="computedAnswerText(li.mark).length > 0" :key="`answer-${j}`" class="right-answer">
+                {{ computedAnswerText(li.mark) }}
+              </span>
+            </template>
+          </template>
+        </p>
+      </div>
+    </AnswerAnalysis>
   </div>
 </template>
 
@@ -347,6 +432,19 @@ export default {
       if (isRight) return '';
       return `(${answerValue})`;
     },
+    // 重做
+    retry() {
+      this.modelEssay.forEach((item) => {
+        item.forEach((li) => {
+          if (li.type === 'input') {
+            li.content = '';
+            li.write_base64 = '';
+          }
+        });
+      });
+      this.selectedWordList = [];
+      this.handleWav([]);
+    },
   },
 };
 </script>

+ 34 - 26
src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue

@@ -58,38 +58,46 @@
         />
       </div>
     </div>
-    <div v-if="isShowRightAnswer" class="right-answer">
-      <div class="title">{{ convertText('正确答案') }}</div>
-      <div
-        v-if="image_url"
-        class="img-box"
-        :style="{
-          background: 'url(' + image_url + ') center / contain no-repeat',
-          width: data.image_width + 'px',
-          height: data.image_height + 'px',
-        }"
-      >
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <div slot="right-answer" class="right-answer">
         <div
-          v-for="(itemP, indexP) in data.input_list"
-          :key="'input' + indexP"
-          :class="['position-item position-item-input', 'active']"
+          v-if="image_url"
+          class="img-box"
           :style="{
-            width: itemP.width,
-            height: itemP.height,
-            left: itemP.x,
-            top: itemP.y,
+            background: 'url(' + image_url + ') center / contain no-repeat',
+            width: data.image_width + 'px',
+            height: data.image_height + 'px',
           }"
         >
-          <el-input
-            v-model="itemP.text"
-            :disabled="disabled"
-            type="textarea"
-            style="height: 100%"
-            :placeholder="convertText('请输入')"
-          />
+          <div
+            v-for="(itemP, indexP) in data.input_list"
+            :key="'input' + indexP"
+            :class="['position-item position-item-input', 'active']"
+            :style="{
+              width: itemP.width,
+              height: itemP.height,
+              left: itemP.x,
+              top: itemP.y,
+            }"
+          >
+            <el-input
+              v-model="itemP.text"
+              :disabled="disabled"
+              type="textarea"
+              style="height: 100%"
+              :placeholder="convertText('请输入')"
+            />
+          </div>
         </div>
       </div>
-    </div>
+    </AnswerAnalysis>
+
     <el-dialog
       v-if="mageazineDetailShow"
       :visible.sync="mageazineDetailShow"

+ 8 - 0
src/views/book/courseware/preview/components/input/InputPreview.vue

@@ -25,6 +25,14 @@
         {{ data.multilingual.find((item) => item.type === getLang())?.translation }}
       </div>
     </div>
+
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    />
   </div>
 </template>
 

+ 61 - 1
src/views/book/courseware/preview/components/judge/JudgePreview.vue

@@ -39,7 +39,6 @@
                 {
                   active: isAnswer(mark, option_type),
                 },
-                computedIsShowRightAnswer(mark, option_type),
               ]"
               @click="selectAnswer(mark, option_type)"
             >
@@ -56,6 +55,67 @@
         </li>
       </ul>
     </div>
+
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <ul slot="right-answer" class="option-list">
+        <li
+          v-for="({ content, mark, paragraph_list }, i) in data.option_list"
+          :key="mark"
+          :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
+          :class="['option-item', { active: isAnswer(mark) }]"
+        >
+          <div
+            :style="{ borderColor: data.unified_attrib?.topic_color }"
+            :class="['option-content', computedIsJudgeRight(mark)]"
+          >
+            <span class="serial-number">{{ computedOptionNumber(i) }}.</span>
+            <PinyinText
+              v-if="isEnable(data.property.view_pinyin)"
+              class="content"
+              :paragraph-list="paragraph_list"
+              :pinyin-position="data.property.pinyin_position"
+              :is-preview="true"
+            />
+            <div
+              v-else
+              class="rich-text"
+              :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+              v-html="convertText(sanitizeHTML(content))"
+            ></div>
+          </div>
+          <div class="option-type">
+            <div
+              v-for="option_type in incertitudeList"
+              :key="option_type"
+              :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
+              :class="[
+                'option-type-item',
+                {
+                  active: isAnswer(mark, option_type),
+                },
+                computedIsShowRightAnswer(mark, option_type),
+              ]"
+              @click="selectAnswer(mark, option_type)"
+            >
+              <SvgIcon
+                v-if="option_type === option_type_list[0].value"
+                icon-class="check-mark"
+                width="17"
+                height="12"
+              />
+              <SvgIcon v-if="option_type === option_type_list[1].value" icon-class="cross" size="12" />
+              <SvgIcon v-if="option_type === option_type_list[2].value" icon-class="circle" size="16" />
+            </div>
+          </div>
+        </li>
+      </ul>
+    </AnswerAnalysis>
   </div>
 </template>
 

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

@@ -34,8 +34,16 @@
           </div>
         </li>
       </ul>
+    </div>
 
-      <div v-if="isShowRightAnswer" class="right-answer">
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <div slot="right-answer" class="right-answer">
         <div class="title">正确答案</div>
         <ul ref="answerList" class="option-list">
           <li v-for="(item, i) in data.option_list" :key="i" class="list-item">
@@ -45,7 +53,7 @@
           </li>
         </ul>
       </div>
-    </div>
+    </AnswerAnalysis>
   </div>
 </template>
 

+ 195 - 178
src/views/book/courseware/preview/components/newWord_template/NewWordTemplatePreview.vue

@@ -208,217 +208,234 @@
         </div>
       </div>
     </div>
-
-    <div v-if="isShowRightAnswer" class="right-answer">
-      <div class="title">{{ convertText('正确答案') }}</div>
-      <div class="box">
-        <div
-          v-for="(item, index) in data.option_list"
-          :key="index"
-          class="item-box"
-          :class="['item-box-' + data.property.model]"
-        >
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <div slot="right-answer" class="right-answer">
+        <div class="box">
           <div
-            class="number-box"
-            :style="{
-              marginTop: isEnable(data.property.view_pinyin)
-                ? '30px'
-                : data.answer_type.indexOf('pinyin') > -1 && data.property.model === 'input'
-                  ? '52px'
-                  : '',
-            }"
+            v-for="(item, index) in data.option_list"
+            :key="index"
+            class="item-box"
+            :class="['item-box-' + data.property.model]"
           >
-            <span
-              class="number"
-              :style="{
-                background:
-                  data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
-              }"
-              >{{ index + 1 }}</span
-            >
-          </div>
-          <div class="pinyin-en" :class="[item.is_example ? 'item-example' : '']">
             <div
-              v-if="isEnable(data.property.view_pinyin) && data.property.model === 'input' && item.is_common_pinyin"
-              class="pinyin"
+              class="number-box"
               :style="{
-                fontSize: data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '',
+                marginTop: isEnable(data.property.view_pinyin)
+                  ? '30px'
+                  : data.answer_type.indexOf('pinyin') > -1 && data.property.model === 'input'
+                    ? '52px'
+                    : '',
               }"
             >
-              {{ item.pinyin }}
-            </div>
-            <div
-              v-if="data.answer_type.indexOf('pinyin') > -1 && data.property.model === 'input' && item.is_common_pinyin"
-              class="inputdv pinyin-common"
-            >
-              <EditDiv
-                :id="'bz' + item.content + index"
-                v-model="item.answer_pinyin"
-                :can-edit="!item.is_example && !disabled"
-                :text-align="'center'"
+              <span
+                class="number"
                 :style="{
-                  fontSize:
-                    data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '16px',
                   background:
-                    data.unified_attrib && data.unified_attrib.assist_color
-                      ? data.unified_attrib.assist_color
-                      : '#deebff',
+                    data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
                 }"
-                @input="changeAnswer(item, index)"
-              />
+                >{{ index + 1 }}</span
+              >
             </div>
-            <div class="items-flex">
+            <div class="pinyin-en" :class="[item.is_example ? 'item-example' : '']">
               <div
-                v-for="(items, indexs) in item.content_list"
-                :key="indexs"
-                class="items"
-                :class="[items.is_example ? 'items-example' : '']"
+                v-if="isEnable(data.property.view_pinyin) && data.property.model === 'input' && item.is_common_pinyin"
+                class="pinyin"
+                :style="{
+                  fontSize:
+                    data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '',
+                }"
               >
-                <div
-                  v-if="
-                    isEnable(data.property.view_pinyin) &&
-                    (data.property.model === 'miao' || (data.property.model === 'input' && !item.is_common_pinyin))
-                  "
-                  class="pinyin"
+                {{ item.pinyin }}
+              </div>
+              <div
+                v-if="
+                  data.answer_type.indexOf('pinyin') > -1 && data.property.model === 'input' && item.is_common_pinyin
+                "
+                class="inputdv pinyin-common"
+              >
+                <EditDiv
+                  :id="'bz' + item.content + index"
+                  v-model="item.answer_pinyin"
+                  :can-edit="!item.is_example && !disabled"
+                  :text-align="'center'"
                   :style="{
                     fontSize:
-                      data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '',
+                      data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '16px',
+                    background:
+                      data.unified_attrib && data.unified_attrib.assist_color
+                        ? data.unified_attrib.assist_color
+                        : '#deebff',
                   }"
-                >
-                  {{ items.pinyin }}
-                </div>
+                  @input="changeAnswer(item, index)"
+                />
+              </div>
+              <div class="items-flex">
                 <div
-                  v-if="
-                    data.answer_type.indexOf('pinyin') > -1 && data.property.model === 'input' && !item.is_common_pinyin
-                  "
-                  class="inputdv pinyin-common"
+                  v-for="(items, indexs) in item.content_list"
+                  :key="indexs"
+                  class="items"
+                  :class="[items.is_example ? 'items-example' : '']"
                 >
-                  <EditDiv
-                    :id="'cz' + items.con + index + indexs"
-                    v-model="items.answer_pinyin"
-                    :can-edit="!items.is_example && !disabled"
-                    :text-align="'center'"
+                  <div
+                    v-if="
+                      isEnable(data.property.view_pinyin) &&
+                      (data.property.model === 'miao' || (data.property.model === 'input' && !item.is_common_pinyin))
+                    "
+                    class="pinyin"
                     :style="{
                       fontSize:
-                        data.unified_attrib && data.unified_attrib.pinyin_size
-                          ? data.unified_attrib.pinyin_size
-                          : '16px',
-                      background:
-                        data.unified_attrib && data.unified_attrib.assist_color
-                          ? data.unified_attrib.assist_color
-                          : '#deebff',
+                        data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '',
                     }"
-                    @input="changeAnswer(item, index, indexs)"
-                  />
-                </div>
-                <div class="items-content">
-                  <template v-if="items && items.type === 'img'">
-                    <el-image
-                      v-if="items.file_list[0]"
-                      class="items-image"
-                      :src="items.file_list[0].file_url"
-                      fit="contain"
+                  >
+                    {{ items.pinyin }}
+                  </div>
+                  <div
+                    v-if="
+                      data.answer_type.indexOf('pinyin') > -1 &&
+                      data.property.model === 'input' &&
+                      !item.is_common_pinyin
+                    "
+                    class="inputdv pinyin-common"
+                  >
+                    <EditDiv
+                      :id="'cz' + items.con + index + indexs"
+                      v-model="items.answer_pinyin"
+                      :can-edit="!items.is_example && !disabled"
+                      :text-align="'center'"
+                      :style="{
+                        fontSize:
+                          data.unified_attrib && data.unified_attrib.pinyin_size
+                            ? data.unified_attrib.pinyin_size
+                            : '16px',
+                        background:
+                          data.unified_attrib && data.unified_attrib.assist_color
+                            ? data.unified_attrib.assist_color
+                            : '#deebff',
+                      }"
+                      @input="changeAnswer(item, index, indexs)"
+                    />
+                  </div>
+                  <div class="items-content">
+                    <template v-if="items && items.type === 'img'">
+                      <el-image
+                        v-if="items.file_list[0]"
+                        class="items-image"
+                        :src="items.file_list[0].file_url"
+                        fit="contain"
+                        :style="{
+                          borderColor:
+                            data.unified_attrib && data.unified_attrib.topic_color
+                              ? data.unified_attrib.topic_color
+                              : '',
+                        }"
+                      />
+                    </template>
+                    <template v-else-if="items && items.type === 'lian'">
+                      <span
+                        class="items-lian"
+                        :style="{
+                          color:
+                            data.unified_attrib && data.unified_attrib.topic_color
+                              ? data.unified_attrib.topic_color
+                              : '',
+                        }"
+                        >{{ items.con }}</span
+                      >
+                    </template>
+                    <Strockplayredline
+                      v-if="items && items.type === 'hanzi'"
+                      :Book_text="items.con"
+                      :play-storkes="isEnable(data.property.is_enable_play_structure)"
+                      :cur-item="
+                        isEnable(data.property.is_enable_high_strokes)
+                          ? data.property.model === 'input'
+                            ? items.high_strokes
+                            : items
+                          : null
+                      "
+                      :type="data.property.model === 'input' ? 'newWord-template-input' : data.type"
+                      :target-div="'newWordTemplatez' + items.con + index + indexs + randomId"
+                      :hz_json="items.hz_info[0].hzDetail.hz_json"
+                      class="hanzi-storck"
+                      :class="[
+                        item.content_list.length > 1 && indexs == 0 ? 'leftBorderRadius' : '',
+
+                        item.content_list.length > 1 && indexs == item.content_list.length - 1
+                          ? 'rightBorderRadius'
+                          : '',
+                        item.content_list.length > 1 && indexs != item.content_list.length - 1 && indexs != 0
+                          ? 'NoborderRadius'
+                          : '',
+                        item.content_list.length > 1 &&
+                        indexs != item.content_list.length - 1 &&
+                        item.content_list[indexs + 1].type !== 'lian'
+                          ? 'NoborderRight'
+                          : '',
+                      ]"
                       :style="{
                         borderColor:
                           data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
                       }"
+                      :play-color="
+                        data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : ''
+                      "
+                      bg-type="tian"
                     />
-                  </template>
-                  <template v-else-if="items && items.type === 'lian'">
-                    <span
-                      class="items-lian"
+                  </div>
+                  <div v-if="data.property.model === 'miao'" class="inputdv">
+                    <EditDiv
+                      v-if="items && items.type === 'hanzi' && items.is_can_input_answer"
+                      :id="'az' + items.con + index + indexs"
+                      v-model="items.answer"
+                      :can-edit="!items.is_example && !disabled"
+                      :text-align="'center'"
                       :style="{
-                        color:
-                          data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
+                        fontSize:
+                          data.unified_attrib && data.unified_attrib.pinyin_size
+                            ? data.unified_attrib.pinyin_size
+                            : '16px',
+                        background:
+                          data.unified_attrib && data.unified_attrib.assist_color
+                            ? data.unified_attrib.assist_color
+                            : '#deebff',
                       }"
-                      >{{ items.con }}</span
-                    >
-                  </template>
-                  <Strockplayredline
-                    v-if="items && items.type === 'hanzi'"
-                    :Book_text="items.con"
-                    :play-storkes="isEnable(data.property.is_enable_play_structure)"
-                    :cur-item="
-                      isEnable(data.property.is_enable_high_strokes)
-                        ? data.property.model === 'input'
-                          ? items.high_strokes
-                          : items
-                        : null
-                    "
-                    :type="data.property.model === 'input' ? 'newWord-template-input' : data.type"
-                    :target-div="'newWordTemplatez' + items.con + index + indexs + randomId"
-                    :hz_json="items.hz_info[0].hzDetail.hz_json"
-                    class="hanzi-storck"
-                    :class="[
-                      item.content_list.length > 1 && indexs == 0 ? 'leftBorderRadius' : '',
-
-                      item.content_list.length > 1 && indexs == item.content_list.length - 1 ? 'rightBorderRadius' : '',
-                      item.content_list.length > 1 && indexs != item.content_list.length - 1 && indexs != 0
-                        ? 'NoborderRadius'
-                        : '',
-                      item.content_list.length > 1 &&
-                      indexs != item.content_list.length - 1 &&
-                      item.content_list[indexs + 1].type !== 'lian'
-                        ? 'NoborderRight'
-                        : '',
-                    ]"
-                    :style="{
-                      borderColor:
-                        data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',
-                    }"
-                    :play-color="
-                      data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : ''
-                    "
-                    bg-type="tian"
-                  />
-                </div>
-                <div v-if="data.property.model === 'miao'" class="inputdv">
-                  <EditDiv
-                    v-if="items && items.type === 'hanzi' && items.is_can_input_answer"
-                    :id="'az' + items.con + index + indexs"
-                    v-model="items.answer"
-                    :can-edit="!items.is_example && !disabled"
-                    :text-align="'center'"
-                    :style="{
-                      fontSize:
-                        data.unified_attrib && data.unified_attrib.pinyin_size
-                          ? data.unified_attrib.pinyin_size
-                          : '16px',
-                      background:
-                        data.unified_attrib && data.unified_attrib.assist_color
-                          ? data.unified_attrib.assist_color
-                          : '#deebff',
-                    }"
-                    @input="changeAnswer(items, index, indexs)"
-                  />
+                      @input="changeAnswer(items, index, indexs)"
+                    />
+                  </div>
                 </div>
               </div>
+              <div
+                v-if="data.answer_type.indexOf('en') > -1 && data.property.model === 'input'"
+                class="inputdv en-common"
+              >
+                <EditDiv
+                  :id="'dz' + item.content + index"
+                  v-model="item.answer_en"
+                  :can-edit="!item.is_example && !disabled"
+                  :text-align="'center'"
+                  :style="{
+                    fontSize:
+                      data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '16px',
+                    background:
+                      data.unified_attrib && data.unified_attrib.assist_color
+                        ? data.unified_attrib.assist_color
+                        : '#deebff',
+                  }"
+                  @input="changeAnswer(item, index)"
+                />
+              </div>
+              <div v-if="data.property.model === 'input'" class="en-common">{{ convertText(item.shiyi) }}</div>
             </div>
-            <div
-              v-if="data.answer_type.indexOf('en') > -1 && data.property.model === 'input'"
-              class="inputdv en-common"
-            >
-              <EditDiv
-                :id="'dz' + item.content + index"
-                v-model="item.answer_en"
-                :can-edit="!item.is_example && !disabled"
-                :text-align="'center'"
-                :style="{
-                  fontSize:
-                    data.unified_attrib && data.unified_attrib.pinyin_size ? data.unified_attrib.pinyin_size : '16px',
-                  background:
-                    data.unified_attrib && data.unified_attrib.assist_color
-                      ? data.unified_attrib.assist_color
-                      : '#deebff',
-                }"
-                @input="changeAnswer(item, index)"
-              />
-            </div>
-            <div v-if="data.property.model === 'input'" class="en-common">{{ convertText(item.shiyi) }}</div>
           </div>
         </div>
       </div>
-    </div>
+    </AnswerAnalysis>
   </div>
 </template>
 

+ 50 - 43
src/views/book/courseware/preview/components/pinyin_base/PinyinBasePreview.vue

@@ -150,52 +150,59 @@
           />
         </template>
       </div>
-      <div v-if="isShowRightAnswer && data.property.fun_type === 'input'" class="right-answer">
-        <div class="title" style="margin: 10px 0">{{ convertText('正确答案') }}</div>
-        <div
-          class="content-box"
-          :class="[data.property.arrange_type === 'horizontal' ? 'content-box-flex' : 'content-box-vertical']"
-        >
-          <div class="first-con">
-            <AudioPlay
-              v-if="data.audio_file_id && data.property.audio_position === 'front'"
-              :file-id="data.audio_file_url"
-              :theme-color="
-                data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : ''
-              "
-            />
-            <div
-              class="option-content"
-              :style="{
-                fontSize: data.unified_attrib && data.unified_attrib.font_size ? data.unified_attrib.font_size : '',
-              }"
-            >
-              <span v-if="data.content_hz" class="items-hz">{{ convertText(data.content_hz) }}</span>
-              <!-- 拼音输入 -->
-              <template v-if="data.property.fun_type === 'input'">
-                <span
-                  v-for="(itemc, indexc) in con_preview"
-                  :key="indexc"
-                  :class="['item-con', itemc.type === 'input' ? 'right' : '']"
-                >
-                  <el-input
-                    v-if="itemc.type === 'input'"
-                    v-model="itemc.answer"
-                    :disabled="true"
-                    :style="[{ width: Math.max(20, itemc.con.length * 10) + 'px' }]"
-                    class="item-input"
-                  />
-                  <span v-else>{{ itemc.con }}</span>
-                </span>
-              </template>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+      <AnswerAnalysis
+        :visible.sync="visibleAnswerAnalysis"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        @closeAnswerAnalysis="closeAnswerAnalysis"
+      >
+        <div slot="right-answer" v-if="data.property.fun_type === 'input'" class="right-answer">
+          <div
+            class="content-box"
+            :class="[data.property.arrange_type === 'horizontal' ? 'content-box-flex' : 'content-box-vertical']"
+          >
+            <div class="first-con">
+              <AudioPlay
+                v-if="data.audio_file_id && data.property.audio_position === 'front'"
+                :file-id="data.audio_file_url"
+                :theme-color="
+                  data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : ''
+                "
+              />
+              <div
+                class="option-content"
+                :style="{
+                  fontSize: data.unified_attrib && data.unified_attrib.font_size ? data.unified_attrib.font_size : '',
+                }"
+              >
+                <span v-if="data.content_hz" class="items-hz">{{ convertText(data.content_hz) }}</span>
+                <!-- 拼音输入 -->
+                <template v-if="data.property.fun_type === 'input'">
+                  <span
+                    v-for="(itemc, indexc) in con_preview"
+                    :key="indexc"
+                    :class="['item-con', itemc.type === 'input' ? 'right' : '']"
+                  >
+                    <el-input
+                      v-if="itemc.type === 'input'"
+                      v-model="itemc.answer"
+                      :disabled="true"
+                      :style="[{ width: Math.max(20, itemc.con.length * 10) + 'px' }]"
+                      class="item-input"
+                    />
+                    <span v-else>{{ itemc.con }}</span>
+                  </span>
+                </template>
+              </div>
+              <AudioPlay
+                v-if="data.audio_file_id && data.property.audio_position === 'back'"
+                :file-id="data.audio_file_url"
+              />
             </div>
-            <AudioPlay
-              v-if="data.audio_file_id && data.property.audio_position === 'back'"
-              :file-id="data.audio_file_url"
-            />
           </div>
         </div>
-      </div>
+      </AnswerAnalysis>
     </div>
   </div>
 </template>

+ 7 - 0
src/views/book/courseware/preview/components/record_input/RecordInputPreview.vue

@@ -27,6 +27,13 @@
           @handleWav="handleWav"
         />
       </div>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+      <AnswerAnalysis
+        :visible.sync="visibleAnswerAnalysis"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        @closeAnswerAnalysis="closeAnswerAnalysis"
+      />
     </div>
   </div>
 </template>

+ 1 - 0
src/views/book/courseware/preview/components/record_input/SoundRecord.vue

@@ -145,6 +145,7 @@ export default {
       default: () => {},
     },
   },
+  inject: ['convertText'],
   data() {
     return {
       recorder: new Recorder({

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

@@ -38,6 +38,49 @@
         </li>
       </ul>
     </div>
+
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <ul
+        slot="right-answer"
+        class="option-list"
+        :style="{ flexDirection: data.property.arrange_type === arrangeTypeList[0].value ? 'row' : 'column' }"
+      >
+        <li
+          v-for="({ content, mark, multilingual, paragraph_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, 'answer-analysis')]"
+          @click="selectAnswer(mark)"
+        >
+          <span :class="[isSingle ? 'radio' : 'checkbox']">
+            <SvgIcon icon-class="check-mark" width="10" height="7" />
+          </span>
+          <span class="serial-number"> {{ computedOptionNumber(i) }}. </span>
+          <PinyinText
+            v-if="isEnable(data.property.view_pinyin)"
+            class="content"
+            :paragraph-list="paragraph_list"
+            :pinyin-position="data.property.pinyin_position"
+            :is-preview="true"
+          />
+          <span
+            v-else
+            class="content rich-text"
+            :style="{ fontSize: type === typeList[0] ? '12pt' : '' }"
+            v-html="convertText(sanitizeHTML(content))"
+          ></span>
+          <div v-if="showLang" class="lang">
+            {{ multilingual.find((item) => item.type === getLang())?.translation }}
+          </div>
+        </li>
+      </ul>
+    </AnswerAnalysis>
   </div>
 </template>
 
@@ -83,6 +126,10 @@ export default {
       if (!type) return number + 1;
       return computeOptionMethods[type](number);
     },
+    /**
+     * 判断选项是否被选中
+     * @param {string} mark - 选项的标识
+     */
     isAnswer(mark) {
       return this.answer.answer_list.includes(mark);
     },
@@ -99,7 +146,13 @@ export default {
 
       this.answer.answer_list = isSelected ? list.filter((item) => item !== mark) : list.concat(mark);
     },
-    computedAnswerClass(mark) {
+    /**
+     * 计算选项的类名
+     * @param {string} mark - 选项的标识
+     * @param {string} type - 计算类名的类型,默认为 'default',可选值为 'answer-analysis'
+     * @returns {Array} - 选项的类名数组
+     */
+    computedAnswerClass(mark, type = 'default') {
       if (!this.isJudgingRightWrong && !this.isShowRightAnswer) {
         return [];
       }
@@ -107,7 +160,7 @@ export default {
       let isRight = this.data.answer.answer_list.includes(mark); // 是否是正确答案
 
       let answerClass = [];
-      if (!isHas && this.isShowRightAnswer) {
+      if (!isHas && this.isShowRightAnswer && type === 'answer-analysis') {
         answerClass = isRight ? ['answer-right'] : [];
       }
       // 判断是否是正确答案

+ 23 - 18
src/views/book/courseware/preview/components/sort/SortPreview.vue

@@ -39,25 +39,30 @@
           </transition-group>
         </draggable>
       </ul>
-
-      <template v-if="isShowRightAnswer && !is_all_right">
-        <div class="right-title">正确答案:</div>
-        <ul class="option-list">
-          <draggable v-model="right_answer_list" animation="300" :disabled="true">
-            <transition-group
-              class="group"
-              :style="{
-                flexDirection: data.property.arrange_direction === arrangeTypeList[0].value ? 'row' : 'column',
-              }"
-            >
-              <li v-for="(item, i) in right_answer_list" :key="i" :class="['drag-item']">
-                <span class="rich-text" v-html="convertText(sanitizeHTML(item.content))"></span>
-              </li>
-            </transition-group>
-          </draggable>
-        </ul>
-      </template>
     </div>
+
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    >
+      <ul slot="right-answer" class="option-list">
+        <draggable v-model="right_answer_list" animation="300" :disabled="true">
+          <transition-group
+            class="group"
+            :style="{
+              flexDirection: data.property.arrange_direction === arrangeTypeList[0].value ? 'row' : 'column',
+            }"
+          >
+            <li v-for="(item, i) in right_answer_list" :key="i" :class="['drag-item']">
+              <span class="rich-text" v-html="convertText(sanitizeHTML(item.content))"></span>
+            </li>
+          </transition-group>
+        </draggable>
+      </ul>
+    </AnswerAnalysis>
   </div>
 </template>
 

+ 250 - 11
src/views/book/courseware/preview/components/table/TablePreview.vue

@@ -204,13 +204,6 @@
                             @handleWav="handleMiniWav($event, item)"
                           />
                         </template>
-                        <span
-                          v-show="computedAnswerText(item, i, j).length > 0"
-                          :key="`answer-${j}`"
-                          class="right-answer"
-                        >
-                          {{ convertText(computedAnswerText(item, i, j)) }}
-                        </span>
                       </template>
                     </div>
                   </template>
@@ -229,6 +222,252 @@
           </tr>
         </table>
       </div>
+      <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+      <AnswerAnalysis
+        :visible.sync="visibleAnswerAnalysis"
+        :answer-list="data.answer_list"
+        :analysis-list="data.analysis_list"
+        @closeAnswerAnalysis="closeAnswerAnalysis"
+      >
+        <div
+          slot="right-answer"
+          class="table-box"
+          :style="{
+            width: isMobile ? '100%' : data.property.width + 'px',
+
+            height: data.property.height + 'px',
+          }"
+        >
+          <table
+            :style="{
+              width: isMobile ? '100%' : table_width + 'px',
+
+              height: data.property.height + 'px',
+            }"
+          >
+            <colgroup>
+              <col v-for="(item, i) in data.col_width" :key="`col-${i}`" :style="{ width: `${item.value}px` }" />
+            </colgroup>
+            <tr v-for="(row, i) in data.option_list" :key="`tr-${i}`">
+              <template v-for="(col, j) in row">
+                <td
+                  :key="col.mark"
+                  :style="{
+                    borderTop: i === 0 ? '1px solid ' + data.property.border_color : '',
+                    borderBottom: '1px solid ' + data.property.border_color,
+
+                    borderLeft:
+                      i === 0 && data.property.first_line_color
+                        ? '1px solid ' + data.property.border_color
+                        : j === 0
+                          ? '2px solid ' +
+                            (data.property.decoration_color
+                              ? data.property.decoration_color
+                              : data.property.border_color)
+                          : '1px dotted ' + data.property.border_color,
+                    borderRight:
+                      i === 0 && data.property.first_line_color
+                        ? '1px solid ' + data.property.border_color
+                        : j === row.length - 1
+                          ? '2px solid ' +
+                            (data.property.decoration_color
+                              ? data.property.decoration_color
+                              : data.property.border_color)
+                          : '1px dotted ' + data.property.border_color,
+                    borderRadius: i === 0 && data.property.first_line_color ? '4px ' : '0',
+                    background:
+                      i === 0 && data.property.first_line_color
+                        ? data.property.first_line_color
+                        : j === 0
+                          ? data.property.first_column_color
+                          : data.mode === 'short' && data.styles.bgColor
+                            ? data.styles.bgColor
+                            : '',
+                  }"
+                >
+                  <div :style="[tdStyle, computedRichStyle(col.content)]" class="cell-wrap">
+                    <template v-if="isEnable(data.property.view_pinyin)">
+                      <div
+                        v-for="(item, index) in col.model_pinyin"
+                        :key="index"
+                        class="pinyin-text"
+                        :class="[index === 0 ? 'pinyin-text-left' : '']"
+                      >
+                        <template v-if="item.type === 'input'">
+                          <template v-if="data.property.fill_type === fillTypeList[0].value">
+                            <el-input
+                              :key="index"
+                              v-model="item.value"
+                              :disabled="disabled"
+                              :class="[...computedAnswerClass(item, i, j)]"
+                              :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
+                            />
+                          </template>
+                          <template v-else-if="data.property.fill_type === fillTypeList[1].value">
+                            <el-popover :key="index" placement="top" trigger="click">
+                              <div class="word-list">
+                                <span
+                                  v-for="{ content, mark } in data.word_list"
+                                  :key="mark"
+                                  class="word-item"
+                                  @click="handleSelectWord(content, mark, item)"
+                                >
+                                  {{ convertText(content) }}
+                                </span>
+                              </div>
+
+                              <el-input
+                                slot="reference"
+                                v-model="item.value"
+                                :readonly="true"
+                                :class="[...computedAnswerClass(item, i, j)]"
+                                :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
+                              />
+                            </el-popover>
+                          </template>
+
+                          <template v-else-if="data.property.fill_type === fillTypeList[2].value">
+                            <span :key="j" class="write-click" @click="handleWriteClick(item)">
+                              <img
+                                v-show="item.write_base64"
+                                style="background-color: #f4f4f4"
+                                :src="item.write_base64"
+                                alt="write-show"
+                              />
+                            </span>
+                          </template>
+
+                          <template v-else-if="data.property.fill_type === fillTypeList[3].value">
+                            <SoundRecordBox
+                              ref="record"
+                              :key="j"
+                              type="mini"
+                              :many-times="false"
+                              class="record-box"
+                              :answer-record-list="data.audio_answer_list"
+                              :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+                              :attrib="data.unified_attrib"
+                              @handleWav="handleMiniWav($event, item)"
+                            />
+                          </template>
+                          <span v-if="data.property.pinyin_position === 'bottom'" class="pinyin">&nbsp;</span>
+                        </template>
+                        <template v-else>
+                          <span v-if="data.property.pinyin_position === 'top'" class="pinyin">
+                            {{ item.pinyin.replace(/\s+/g, '') }}
+                          </span>
+                          <span :style="{ ...item.activeTextStyle }">
+                            {{ convertText(item.text) }}
+                          </span>
+                          <span v-if="data.property.pinyin_position === 'bottom'" class="pinyin">{{
+                            item.pinyin.replace(/\s+/g, '')
+                          }}</span>
+                        </template>
+                        <span
+                          v-show="computedAnswerText(item, i, j).length > 0"
+                          :key="`answer-${j}`"
+                          class="right-answer"
+                        >
+                          {{ convertText(computedAnswerText(item, i, j)) }}
+                        </span>
+                      </div>
+                    </template>
+                    <template v-else>
+                      <div v-for="(item, index) in col.model_essay" :key="index" :style="[tdStyle]">
+                        <span
+                          v-if="item.type === 'text'"
+                          :key="index"
+                          :style="{
+                            fontSize:
+                              data.unified_attrib && data.unified_attrib.font_size ? data.unified_attrib.font_size : '',
+                            fontFamily: data.unified_attrib && data.unified_attrib.font ? data.unified_attrib.font : '',
+                          }"
+                          v-html="convertText(sanitizeHTML(item.value))"
+                        ></span>
+                        <template v-if="item.type === 'input'">
+                          <template v-if="data.property.fill_type === fillTypeList[0].value">
+                            <el-input
+                              :key="index"
+                              v-model="item.value"
+                              :disabled="disabled"
+                              :class="[...computedAnswerClass(item, i, j)]"
+                              :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
+                            />
+                          </template>
+                          <template v-else-if="data.property.fill_type === fillTypeList[1].value">
+                            <el-popover :key="index" placement="top" trigger="click">
+                              <div class="word-list">
+                                <span
+                                  v-for="{ content, mark } in data.word_list"
+                                  :key="mark"
+                                  class="word-item"
+                                  @click="handleSelectWord(content, mark, item)"
+                                >
+                                  {{ convertText(content) }}
+                                </span>
+                              </div>
+
+                              <el-input
+                                slot="reference"
+                                v-model="item.value"
+                                :readonly="true"
+                                :class="[...computedAnswerClass(item, i, j)]"
+                                :style="[{ width: Math.max(40, item.value.length * 21.3) + 'px' }]"
+                              />
+                            </el-popover>
+                          </template>
+
+                          <template v-else-if="data.property.fill_type === fillTypeList[2].value">
+                            <span :key="j" class="write-click" @click="handleWriteClick(item)">
+                              <img
+                                v-show="item.write_base64"
+                                style="background-color: #f4f4f4"
+                                :src="item.write_base64"
+                                alt="write-show"
+                              />
+                            </span>
+                          </template>
+
+                          <template v-else-if="data.property.fill_type === fillTypeList[3].value">
+                            <SoundRecordBox
+                              ref="record"
+                              :key="j"
+                              type="mini"
+                              :many-times="false"
+                              class="record-box"
+                              :answer-record-list="data.audio_answer_list"
+                              :task-model="isJudgingRightWrong ? 'ANSWER' : ''"
+                              :attrib="data.unified_attrib"
+                              @handleWav="handleMiniWav($event, item)"
+                            />
+                          </template>
+                          {{ computedAnswerText(item, i, j) }}
+                          <span
+                            v-show="computedAnswerText(item, i, j).length > 0"
+                            :key="`answer-${j}`"
+                            class="right-answer"
+                          >
+                            {{ convertText(computedAnswerText(item, i, j)) }}
+                          </span>
+                        </template>
+                      </div>
+                    </template>
+                  </div>
+                  <span v-if="showLang" class="multilingual" :style="[tdStyle, computedRichStyle(col.content)]">
+                    {{
+                      multilingualTextList[getLang()] &&
+                      multilingualTextList[getLang()][i] &&
+                      multilingualTextList[getLang()][i][j]
+                        ? multilingualTextList[getLang()][i][j]
+                        : ''
+                    }}
+                  </span>
+                </td>
+              </template>
+            </tr>
+          </table>
+        </div>
+      </AnswerAnalysis>
     </div>
     <WriteDialog :visible.sync="writeVisible" @confirm="handleWriteConfirm" />
   </div>
@@ -294,7 +533,7 @@ export default {
     },
     isJudgingRightWrong(val) {
       if (!val) return;
-      this.data.answer_list = this.answer.answer_list;
+      this.data.answer_lists = this.answer.answer_list;
       // this.answer.answer_list.forEach(({ mark, value }) => {
       //   this.modelEssay.forEach((item) => {
       //     item.forEach((li) => {
@@ -330,13 +569,13 @@ export default {
         this.$set(this.multilingualTextList, item.type, chunkedArr);
       });
       if (!this.isJudgingRightWrong) {
-        this.answer.answer_list = this.data.answer_list;
+        this.answer.answer_list = this.data.answer_lists;
       }
     },
     computedAnswerText(item, i, j) {
       if (!this.isShowRightAnswer) return '';
       let answerOption =
-        this.data.answer_list[i] && this.data.answer_list[i][j] ? this.data.answer_list[i][j].answer : '';
+        this.data.answer_lists[i] && this.data.answer_lists[i][j] ? this.data.answer_lists[i][j].answer : '';
       let answerOptionList = answerOption.split('\n');
       if (!item) return '';
       let selectValue = item.value ? item.value.trim() : '';
@@ -354,7 +593,7 @@ export default {
         return '';
       }
       let answerOption =
-        this.data.answer_list[i] && this.data.answer_list[i][j] ? this.data.answer_list[i][j].answer : '';
+        this.data.answer_lists[i] && this.data.answer_lists[i][j] ? this.data.answer_lists[i][j].answer : '';
       let answerOptionList = answerOption.split('\n');
       if (!item) return '';
       let selectValue = item.value ? item.value.trim() : '';

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

@@ -25,6 +25,13 @@
       @click="lookReport"
       >{{ convertText('查看答题报告') }}</el-button
     >
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    />
     <el-dialog
       v-if="visible"
       :visible.sync="visible"

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

@@ -208,6 +208,14 @@
         />
       </div>
     </div>
+
+    <PreviewOperation @showAnswerAnalysis="showAnswerAnalysis" />
+    <AnswerAnalysis
+      :visible.sync="visibleAnswerAnalysis"
+      :answer-list="data.answer_list"
+      :analysis-list="data.analysis_list"
+      @closeAnswerAnalysis="closeAnswerAnalysis"
+    />
   </div>
 </template>
 

+ 9 - 0
src/web_preview/index.vue

@@ -382,6 +382,7 @@ export default {
       getProjectId: () => this.projectId,
       getSelectId: () => this.select_node,
       getTitleList: () => this.title_list,
+      getPermissionControl: () => this.permissionControl,
     };
   },
   data() {
@@ -496,6 +497,14 @@ export default {
       multimediaLoadingStates: true,
       isFullScreen: false, // 是否全屏状态
       title_list: [],
+      // 模拟答题权限控制
+      permissionControl: {
+        can_answer: false, // 可作答
+        can_judge_correct: false, // 可判断对错(客观题)
+        can_show_answer: false, // 可查看答案
+        can_correct: false, // 可批改
+        can_check_correct: false, // 可查看批改
+      },
     };
   },
   computed: {