dsy пре 2 недеља
родитељ
комит
2bf550bf12
31 измењених фајлова са 564 додато и 201 уклоњено
  1. 1 1
      .env
  2. 1 1
      package.json
  3. BIN
      src/assets/icon/arrow-down.png
  4. BIN
      src/assets/icon/arrow-up.png
  5. BIN
      src/assets/icon/back-top.png
  6. BIN
      src/assets/icon/sidebar-audio.png
  7. BIN
      src/assets/icon/sidebar-audit.png
  8. BIN
      src/assets/icon/sidebar-collect.png
  9. BIN
      src/assets/icon/sidebar-file.png
  10. BIN
      src/assets/icon/sidebar-fullscreen.png
  11. BIN
      src/assets/icon/sidebar-image.png
  12. BIN
      src/assets/icon/sidebar-knowledge.png
  13. BIN
      src/assets/icon/sidebar-mindmap.png
  14. BIN
      src/assets/icon/sidebar-note.png
  15. BIN
      src/assets/icon/sidebar-search.png
  16. BIN
      src/assets/icon/sidebar-setting.png
  17. BIN
      src/assets/icon/sidebar-toolkit.png
  18. BIN
      src/assets/icon/sidebar-totalResources.png
  19. BIN
      src/assets/icon/sidebar-translate.png
  20. BIN
      src/assets/icon/sidebar-video.png
  21. 89 0
      src/components/AuditRemark.vue
  22. 417 151
      src/components/CommonPreview.vue
  23. 6 0
      src/icons/svg/catalogue.svg
  24. 1 1
      src/layouts/default/index.vue
  25. 3 3
      src/layouts/home/footer/index.vue
  26. 2 0
      src/styles/variables.scss
  27. 7 7
      src/views/book/courseware/preview/components/article/NormalModelChs.vue
  28. 8 8
      src/views/book/courseware/preview/components/article/Practicechs.vue
  29. 2 2
      src/views/book/courseware/preview/components/character_structure/CharacterStructurePreview.vue
  30. 1 1
      src/views/book/courseware/preview/components/image_text/ImageTextPreview.vue
  31. 26 26
      src/views/book/courseware/preview/components/new_word/NewWordPreview.vue

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2025.10.30'
+VUE_APP_VERSION = '2025.11.10'

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep_page",
-  "version": "2025.10.30",
+  "version": "2025.11.10",
   "private": true,
   "main": "main.js",
   "description": "智慧梧桐数字教材编辑器",

BIN
src/assets/icon/arrow-down.png


BIN
src/assets/icon/arrow-up.png


BIN
src/assets/icon/back-top.png


BIN
src/assets/icon/sidebar-audio.png


BIN
src/assets/icon/sidebar-audit.png


BIN
src/assets/icon/sidebar-collect.png


BIN
src/assets/icon/sidebar-file.png


BIN
src/assets/icon/sidebar-fullscreen.png


BIN
src/assets/icon/sidebar-image.png


BIN
src/assets/icon/sidebar-knowledge.png


BIN
src/assets/icon/sidebar-mindmap.png


BIN
src/assets/icon/sidebar-note.png


BIN
src/assets/icon/sidebar-search.png


BIN
src/assets/icon/sidebar-setting.png


BIN
src/assets/icon/sidebar-toolkit.png


BIN
src/assets/icon/sidebar-totalResources.png


BIN
src/assets/icon/sidebar-translate.png


BIN
src/assets/icon/sidebar-video.png


+ 89 - 0
src/components/AuditRemark.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="audit-remark">
+    <h5>审校批注</h5>
+    <ul v-if="remarkList.length > 0">
+      <li v-for="{ id: remarkId, content, remark_person_name, remark_time } in remarkList" :key="remarkId">
+        <p v-html="content"></p>
+        <div v-if="isAudit" class="remark-bottom">
+          <span>{{ remark_person_name + ':' + remark_time }}</span>
+          <el-button type="text" class="delete-btn" @click="deleteRemarks(remarkId)">删除</el-button>
+        </div>
+      </li>
+    </ul>
+    <p v-else style="text-align: center">暂无批注</p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AuditRemark',
+  props: {
+    isAudit: {
+      type: Boolean,
+      default: false,
+    },
+    remarkList: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    deleteRemarks(remarkId) {
+      this.$emit('deleteRemarks', remarkId);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.audit-remark {
+  width: 240px;
+  height: 100%;
+  overflow: auto;
+  border: 1px solid #e5e5e5;
+
+  h5 {
+    padding: 0 5px;
+    margin: 0;
+    font-size: 18px;
+    line-height: 40px;
+    background: #f2f3f5;
+  }
+
+  .delete-btn {
+    padding-left: 10px;
+    color: #f44444;
+    border-left: 1px solid #e5e5e5;
+  }
+
+  ul {
+    height: calc(100% - 40px);
+    overflow: auto;
+
+    li {
+      border-bottom: 1px solid #e5e5e5;
+
+      > p {
+        padding: 5px;
+      }
+
+      :deep p {
+        margin: 0;
+      }
+
+      .remark-bottom {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 5px;
+        font-size: 14px;
+        color: #555;
+        border-top: 1px solid #e5e5e5;
+      }
+    }
+  }
+}
+</style>

+ 417 - 151
src/components/CommonPreview.vue

@@ -1,11 +1,7 @@
 <template>
   <div class="common-preview">
     <div class="common-preview__header">
-      <div class="menu-container">
-        <MenuPopover :id="id" :node-list="node_list" :book-name="courseware_info.book_name" @selectNode="selectNode" />
-      </div>
       <div class="courseware">
-        <span class="name-path">{{ courseware_info.name_path }}</span>
         <span class="flow-nodename">{{ courseware_info.cur_audit_flow_node_name }}</span>
         <slot name="middle" :courseware="courseware_info"></slot>
         <div class="group">
@@ -26,7 +22,42 @@
     </div>
 
     <div class="audit-content">
-      <div ref="previewMain" class="main-container">
+      <!-- 左侧菜单栏 - 收缩 -->
+      <aside v-if="navigationShow" class="left-menu">
+        <div class="courseware-info">
+          <div class="cover-image"></div>
+          <div class="info-content">
+            <div class="catalogue-icon">
+              <SvgIcon icon-class="catalogue" size="54" />
+            </div>
+            <div class="courseware">
+              <div class="name nowrap-ellipsis" :title="courseware_info.book_name">
+                {{ courseware_info.book_name }}
+              </div>
+            </div>
+          </div>
+        </div>
+        <!-- 教材章节树 -->
+        <div class="courseware-tree">
+          <div
+            v-for="{ id: nodeId, name, deep, is_leaf_chapter } in node_list"
+            :key="nodeId"
+            :class="['menu-item', { active: curSelectId === nodeId }, { courseware: isTrue(is_leaf_chapter) }]"
+            :style="computedNameStyle(deep, isTrue(is_leaf_chapter))"
+            @click="selectChapterNode(nodeId, isTrue(is_leaf_chapter))"
+          >
+            <span class="name nowrap-ellipsis" :title="name">
+              {{ name }}
+            </span>
+          </div>
+        </div>
+      </aside>
+
+      <div ref="previewMain" class="main-container" :style="{ paddingRight: sidebarShow ? '15px' : '315px' }">
+        <div v-if="!navigationShow" class="catalogue-bar">
+          <SvgIcon icon-class="catalogue" size="54" />
+        </div>
+
         <main :class="['preview-main', { 'no-audit': !isShowAudit }]">
           <div class="preview-left"></div>
           <CoursewarePreview
@@ -46,32 +77,61 @@
           />
           <div class="preview-right"></div>
         </main>
-      </div>
-      <div v-if="isShowAudit" class="remark-list">
-        <h5>审校批注</h5>
-        <ul v-if="remark_list.length > 0">
-          <li v-for="{ id: remarkId, content, remark_person_name, remark_time } in remark_list" :key="remarkId">
-            <!-- eslint-disable-next-line vue/no-v-html -->
-            <p v-html="content"></p>
-            <div v-if="isAudit" class="remark-bottom">
-              <span>{{ remark_person_name + ':' + remark_time }}</span>
-              <el-button type="text" class="delete-btn" @click="deleteRemarks(remarkId)">删除</el-button>
+
+        <!-- 右侧菜单栏 - 收缩 -->
+        <aside v-if="!sidebarShow" class="sidebar-bar">
+          <aside class="toolbar">
+            <div class="toolbar-special">
+              <img :src="require('@/assets/icon/sidebar-fullscreen.png')" alt="全屏" />
+              <img :src="require('@/assets/icon/sidebar-toolkit.png')" alt="工具箱" />
+              <img :src="require(`@/assets/icon/arrow-down.png`)" alt="伸缩" @click="toggleSidebarShow" />
             </div>
-          </li>
-        </ul>
-        <p v-else style="text-align: center">暂无批注</p>
+          </aside>
+        </aside>
       </div>
-      <div ref="sidebarMenu" class="sidebar">
-        <div
-          v-for="{ icon, title, handle, param } in sidebarIconList"
-          :key="icon"
-          :title="title"
-          class="sidebar-icon"
-          @click="handleSidebarClick(handle, param)"
-        >
-          <SvgIcon :icon-class="`sidebar-${icon}`" size="24" />
-        </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="全屏" />
+            <img :src="require('@/assets/icon/sidebar-toolkit.png')" alt="工具箱" />
+          </div>
+          <div v-if="sidebarShow" class="toolbar-list">
+            <div
+              v-for="{ icon, title, handle, param } in sidebarIconList"
+              :key="icon"
+              :class="['sidebar-item', { active: curToolbarIcon === icon }]"
+              :title="title"
+              @click="handleSidebarClick(handle, param, icon)"
+            >
+              <div
+                class="sidebar-icon icon-mask"
+                :style="{
+                  backgroundColor: curToolbarIcon === icon ? '#fff' : '#1E2129',
+                  maskImage: `url(${require(`@/assets/icon/sidebar-${icon}.png`)})`,
+                }"
+              ></div>
+            </div>
+          </div>
+          <div class="adjustable" @click="toggleSidebarShow">
+            <img :src="require(`@/assets/icon/arrow-up.png`)" alt="伸缩" />
+          </div>
+        </aside>
+        <div class="content">
+          <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>
+
       <el-drawer
         custom-class="custom-drawer"
         :visible="drawerType.length > 0"
@@ -150,12 +210,12 @@
 
 <script>
 import CoursewarePreview from '@/views/book/courseware/preview/CoursewarePreview.vue';
-import MenuPopover from '@/views/personal_workbench/common/MenuPopover.vue';
 import RichText from '@/components/RichText.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 AuditRemark from '@/components/AuditRemark.vue';
 import * as OpenCC from 'opencc-js';
 
 import {
@@ -179,11 +239,11 @@ export default {
   name: 'CommonPreview',
   components: {
     CoursewarePreview,
-    MenuPopover,
     RichText,
     MindMap,
     AudioPlay,
     VideoPlay,
+    AuditRemark,
   },
   provide() {
     return {
@@ -217,6 +277,24 @@ export default {
     },
   },
   data() {
+    const sidebarIconList = [
+      { icon: 'search', title: '搜索', handle: '', param: {} },
+      { icon: 'mindmap', title: '思维导图', handle: 'openMindMap', param: {} },
+      { icon: 'knowledge', title: '知识图谱', handle: '', param: {} },
+      { icon: 'totalResources', title: '总资源', handle: '', param: {} },
+      { icon: 'collect', title: '收藏', handle: '', param: {} },
+      { 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: 'note', title: '笔记', handle: '', param: {} },
+      { icon: 'translate', title: '翻译', handle: '', param: {} },
+      { icon: 'setting', title: '设置', handle: '', param: {} },
+    ];
+
+    if (this.isShowAudit) {
+      sidebarIconList.push({ icon: 'audit', title: '审校批注', handle: 'openAudit', param: {} });
+    }
+
     return {
       select_node: this.id,
       courseware_info: {
@@ -254,18 +332,8 @@ export default {
         y: -1,
         componentId: 'WHOLE',
       },
-      sidebarIconList: [
-        { icon: 'search', title: '搜索', handle: '', param: {} },
-        { icon: 'mindmap', title: '思维导图', handle: 'openMindMap', param: {} },
-        { icon: 'connect', title: '连接', handle: '', param: {} },
-        { 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: 'text', title: '文本', handle: '', param: {} },
-        { icon: 'file', title: '文件', handle: '', param: {} },
-        { icon: 'collect', title: '收藏', handle: '', param: {} },
-        { icon: 'setting', title: '设置', handle: '', param: {} },
-      ],
+      curToolbarIcon: this.isShowAudit ? 'audit' : '',
+      sidebarIconList,
       visibleMindMap: false,
       isChildDataLoad: false,
       mindMapJsonData: {}, // 思维导图json数据
@@ -289,6 +357,9 @@ export default {
       isJudgeCorrect: false,
       isShowAnswer: false,
       unified_attrib: {},
+      curSelectId: this.id,
+      navigationShow: true,
+      sidebarShow: true,
     };
   },
   computed: {
@@ -414,21 +485,19 @@ export default {
     // 审校批注列表
     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) => {
+
+        this.remark_list_obj = 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;
       });
     },
     addRemark(selectNode, x, y, componentId) {
@@ -497,11 +566,13 @@ export default {
      * 处理侧边栏图标点击事件
      * @param {string} handle - 处理函数名
      * @param {any} param - 处理函数参数
+     * @param {string} icon - 图标名称
      */
-    handleSidebarClick(handle, param) {
+    handleSidebarClick(handle, param, icon) {
       if (typeof handle === 'string' && handle && typeof this[handle] === 'function') {
         this[handle](param);
       }
+      this.curToolbarIcon = icon;
     },
 
     openMindMap() {
@@ -554,13 +625,13 @@ export default {
         return;
       }
       this.drawerType = type;
-      this.drawerVisible = true;
       this.$nextTick(() => {
         this.cur_page = 1;
         this.file_list = [];
         this.loadMore();
       });
     },
+    openAudit() {},
     // 加载更多数据
     loadMore() {
       if (this.disabled) return;
@@ -610,6 +681,49 @@ export default {
     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',
+      };
+    },
+    /**
+     * 切换左侧导航栏显示与隐藏
+     */
+    toggleNavigationShow() {
+      this.navigationShow = !this.navigationShow;
+    },
+    /**
+     * 切换右侧工具栏显示与隐藏
+     */
+    toggleSidebarShow() {
+      this.sidebarShow = !this.sidebarShow;
+    },
+    backTop() {
+      this.$refs.previewMain.scrollTo({
+        top: 0,
+        left: 0,
+        behavior: 'smooth',
+      });
+    },
   },
 };
 </script>
@@ -617,6 +731,8 @@ export default {
 <style lang="scss" scoped>
 @use '@/styles/mixin.scss' as *;
 
+$total-width: $courseware-width + $courseware-left-margin + $courseware-right-margin;
+
 .common-preview {
   &__header {
     position: sticky;
@@ -632,14 +748,6 @@ export default {
     border-top: $border;
     border-bottom: $border;
 
-    > .menu-container {
-      display: flex;
-      justify-content: space-between;
-      width: 360px;
-      padding: 4px 8px;
-      border-right: $border;
-    }
-
     > .courseware {
       display: flex;
       flex-grow: 1;
@@ -648,15 +756,6 @@ export default {
       justify-content: space-between;
       height: 40px;
 
-      .name-path {
-        min-width: 200px;
-        height: 40px;
-        padding: 4px 8px;
-        font-size: 14px;
-        line-height: 32px;
-        border-right: $border;
-      }
-
       .lang-select {
         :deep .el-input {
           width: 100px;
@@ -703,22 +802,91 @@ export default {
   }
 
   .main-container {
+    position: relative;
     flex: 1;
     min-width: 1110px;
+    padding: 15px 0;
     overflow: auto;
     background: url('@/assets/preview-bg.png') repeat;
+
+    &.fullscreen {
+      display: flex;
+
+      & .catalogue-bar {
+        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($courseware-width + $courseware-left-margin + $courseware-right-margin);
-    min-width: calc($courseware-width + $courseware-left-margin + $courseware-right-margin);
+    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 4px rgba(0, 0, 0, 10%);
+    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 40%);
 
     .preview-left {
       width: $courseware-left-margin;
@@ -742,124 +910,222 @@ export default {
   .audit-content {
     display: flex;
     min-width: 1400px;
-    height: calc(100vh - 175px);
-
-    .remark-list {
-      width: 300px;
-      margin-left: 20px;
-      overflow: auto;
-      border: 1px solid #e5e5e5;
-
-      h5 {
-        padding: 0 5px;
-        margin: 0;
-        font-size: 18px;
-        line-height: 40px;
-        background: #f2f3f5;
-      }
+    height: calc(100vh - 166px);
+
+    .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 {
+          width: 111px;
+          height: 157px;
+          background-color: rgba(229, 229, 229, 100%);
+        }
 
-      .delete-btn {
-        padding-left: 10px;
-        color: #f44444;
-        border-left: 1px solid #e5e5e5;
+        .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;
+            }
+          }
+        }
       }
 
-      ul {
-        height: calc(100% - 40px);
+      .courseware-tree {
+        display: flex;
+        flex: 1;
+        flex-direction: column;
+        row-gap: 8px;
+        padding: 12px;
+        margin-top: 12px;
         overflow: auto;
 
-        li {
-          border-bottom: 1px solid #e5e5e5;
+        .menu-item {
+          display: flex;
+          align-items: center;
+
+          &:not(.courseware) {
+            font-weight: bold;
+          }
+
+          &.courseware {
+            &:hover {
+              .name {
+                background-color: #f3f3f3;
+              }
+            }
+          }
+
+          .svg-icon {
+            margin-left: 4px;
 
-          > p {
-            padding: 5px;
+            &.my-edit-task {
+              color: $right-color;
+            }
           }
 
-          :deep p {
-            margin: 0;
+          .name {
+            flex: 1;
+            padding: 4px 8px 4px 4px;
+            border-radius: 4px;
           }
 
-          .remark-bottom {
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            padding: 0 5px;
-            font-size: 14px;
-            color: #555;
-            border-top: 1px solid #e5e5e5;
+          &.active {
+            .name {
+              font-weight: bold;
+              color: #4095e5;
+            }
           }
         }
       }
     }
 
     .sidebar {
+      position: relative;
       display: flex;
-      flex-direction: column;
-      row-gap: 16px;
-      align-items: center;
-      height: 100%;
-      padding: 12px 8px;
-      margin-left: 8px;
-      box-shadow: -4px 0 4px rgba(0, 0, 0, 10%);
-
-      &-icon {
-        cursor: pointer;
-      }
-    }
+      width: $sidebar-width;
 
-    .el-drawer__body {
-      .scroll-container {
+      .toolbar {
         display: flex;
         flex-direction: column;
-        row-gap: 8px;
-        margin: 6px;
+        align-items: center;
+        width: 60px;
+        height: 100%;
+        background-color: rgba(247, 248, 250, 100%);
+
+        img {
+          cursor: pointer;
+        }
 
-        .list-item {
+        &-special {
           display: flex;
+          flex-direction: column;
+          row-gap: 16px;
+          margin-bottom: 24px;
+        }
+
+        &-list {
+          display: flex;
+          flex-direction: column;
+          row-gap: 16px;
           align-items: center;
-          cursor: pointer;
-          border: 1px solid #ccc;
-          border-radius: 8px;
+          width: 100%;
 
-          :deep .el-slider {
-            .el-slider__runway {
-              background-color: #eee;
+          .sidebar-item {
+            width: 100%;
+            text-align: center;
+
+            .sidebar-icon {
+              width: 36px;
+              height: 36px;
+              cursor: pointer;
             }
-          }
 
-          :deep .audio-middle {
-            width: calc(25vw - 40px);
-            border: none;
-            border-radius: 8px;
+            &.active {
+              background-color: #4095e5;
+            }
           }
+        }
+      }
 
-          .el-image {
-            display: flex;
-            width: 30%;
-            min-width: 30%;
-            height: 90px;
-            margin: 6px;
-            background-color: #ccc;
-            border-radius: 8px;
-          }
+      .content {
+        flex: 1;
+        background-color: #fff;
+      }
 
-          .video-play {
-            width: 30%;
-            min-width: 30%;
-            margin: 6px;
-          }
+      .back-top {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        display: flex;
+        place-content: center center;
+        align-items: center;
+        width: 60px;
+        height: 60px;
+        cursor: pointer;
+      }
+    }
+  }
 
-          .text-box {
-            word-break: break-word;
+  .el-drawer__body {
+    .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;
           }
         }
-      }
 
-      p {
-        color: #999;
-        text-align: center;
+        :deep .audio-middle {
+          width: calc(25vw - 40px);
+          border: none;
+          border-radius: 8px;
+        }
+
+        .el-image {
+          display: flex;
+          width: 30%;
+          min-width: 30%;
+          height: 90px;
+          margin: 6px;
+          background-color: #ccc;
+          border-radius: 8px;
+        }
+
+        .video-play {
+          width: 30%;
+          min-width: 30%;
+          margin: 6px;
+        }
+
+        .text-box {
+          word-break: break-word;
+        }
       }
     }
+
+    p {
+      color: #999;
+      text-align: center;
+    }
   }
 }
 

+ 6 - 0
src/icons/svg/catalogue.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" width="52.5" height="52.5" style="" filter="none">
+    
+    <g>
+    <path d="M8 6h-2v2h2v-2zM28 6h-18v2h18v-2zM8 14h-2v2h2v-2zM28 14h-18v2h18v-2zM8 22h-2v2h2v-2zM28 22h-18v2h18v-2z" fill="rgba(64,149,229,1)"></path>
+    </g>
+  </svg>

+ 1 - 1
src/layouts/default/index.vue

@@ -2,7 +2,7 @@
   <div class="app">
     <LayoutHeader />
     <LayoutBreadcrumb :is-show-breadcrumb.sync="isShowBreadcrumb" />
-    <div class="app-container" :style="{ height: `calc(100vh - 54px ${isShowBreadcrumb ? '- 56px' : ''})` }">
+    <div class="app-container" :style="{ height: `calc(100vh - 64px ${isShowBreadcrumb ? '- 56px' : ''})` }">
       <router-view />
     </div>
   </div>

+ 3 - 3
src/layouts/home/footer/index.vue

@@ -1,8 +1,8 @@
 <template>
-  <div class="home-footer">
+  <footer class="home-footer">
     <div class="email"></div>
     <div class="version">版本:{{ version }}</div>
-  </div>
+  </footer>
 </template>
 
 <script>
@@ -17,7 +17,7 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.home-footer {
+footer.home-footer {
   display: flex;
   align-items: center;
   justify-content: flex-end;

+ 2 - 0
src/styles/variables.scss

@@ -30,3 +30,5 @@ $courseware-bottom-margin: 65px; // 教材底部页边距
 $component-spacing: 24px; // 组件间间距
 $title-content-spacing: 65px; // 标题与内容间距
 $border-component-spacing: 10px; // 组件边框与组件内容间距
+$catalogue-width: 300px; // 目录宽度
+$sidebar-width: 300px; // 工具栏宽度

+ 7 - 7
src/views/book/courseware/preview/components/article/NormalModelChs.vue

@@ -22,8 +22,8 @@
             :width="colLength == 2 ? 200 : 700"
             :ed="ed"
             type="audioLine"
-            @emptyEd="emptyEd"
             :attrib="attrib"
+            @emptyEd="emptyEd"
           />
         </template>
       </div>
@@ -446,9 +446,9 @@
                 </div>
               </div>
               <div
+                v-if="curQue.property.multilingual_position === 'para'"
                 class="multilingual-para"
                 :class="[item.isTitle ? 'multilingual-para-center' : '']"
-                v-if="curQue.property.multilingual_position === 'para'"
               >
                 {{
                   curQue.detail[index].multilingualTextList && curQue.detail[index].multilingualTextList[multilingual]
@@ -866,9 +866,9 @@
               </div>
             </div>
             <div
+              v-if="curQue.property.multilingual_position === 'para'"
               class="multilingual-para"
               :class="[item.isTitle ? 'multilingual-para-center' : '']"
-              v-if="curQue.property.multilingual_position === 'para'"
             >
               {{ multilingualTextList[multilingual] ? multilingualTextList[multilingual].join(' ') : '' }}
             </div>
@@ -876,11 +876,11 @@
         </div>
       </template>
     </template>
-    <div class="multilingual" v-for="(items, indexs) in curQue.detail" :key="indexs">
+    <div v-for="(items, indexs) in curQue.detail" :key="indexs" class="multilingual">
       <div
+        v-if="curQue.property.multilingual_position === 'all'"
         class="multilingual-para"
         :class="[items.isTitle ? 'multilingual-para-center' : '']"
-        v-if="curQue.property.multilingual_position === 'all'"
       >
         {{
           items.multilingualTextList && items.multilingualTextList[multilingual]
@@ -910,8 +910,8 @@
             :width="colLength == 2 ? 200 : 700"
             :ed="ed"
             type="audioLine"
-            @emptyEd="emptyEd"
             :attrib="attrib"
+            @emptyEd="emptyEd"
           />
         </template>
       </div>
@@ -1283,7 +1283,7 @@ export default {
       if (_this.NumberList.indexOf(noteNum) > -1) {
         for (let i = 0; i < _this.NumberList.length; i++) {
           if (_this.NumberList[i] === noteNum) {
-            noteIndex = String(i) + '';
+            noteIndex = `${String(i)}`;
             break;
           }
         }

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

@@ -23,9 +23,9 @@
             :mp3-source="curQue.mp3_list[0].source"
             :ed="ed"
             type="audioLine"
+            :attrib="attrib"
             @handleChangeStopAudio="handleChangeStopAudio"
             @emptyEd="emptyEd"
-            :attrib="attrib"
           />
         </template>
       </div>
@@ -354,9 +354,9 @@
             </div>
           </div>
           <div
+            v-if="curQue.property.multilingual_position === 'para'"
             class="multilingual-para"
             :class="[item.isTitle ? 'multilingual-para-center' : '']"
-            v-if="curQue.property.multilingual_position === 'para'"
           >
             {{
               multilingualTextList[multilingual] && multilingualTextList[multilingual][index]
@@ -376,10 +376,10 @@
           >
             <div class="Soundrecord-content-inner">
               <Soundrecord
+                v-if="refresh"
                 type="promax"
                 class="luyin-box"
                 :TaskModel="TaskModel"
-                v-if="refresh"
                 :answer-record-list="
                   curQue.Bookanswer.practiceModel[index] && curQue.Bookanswer.practiceModel[index].recordList
                 "
@@ -416,11 +416,11 @@
         </div> -->
       </div>
     </template>
-    <div class="multilingual" v-for="(items, indexs) in curQue.detail" :key="indexs">
+    <div v-for="(items, indexs) in curQue.detail" :key="indexs" class="multilingual">
       <div
+        v-if="curQue.property.multilingual_position === 'all'"
         class="multilingual-para"
         :class="[items.isTitle ? 'multilingual-para-center' : '']"
-        v-if="curQue.property.multilingual_position === 'all'"
       >
         {{
           items.multilingualTextList && items.multilingualTextList[multilingual]
@@ -450,9 +450,9 @@
             :mp3-source="curQue.mp3_list[0].source"
             :ed="ed"
             type="audioLine"
+            :attrib="attrib"
             @handleChangeStopAudio="handleChangeStopAudio"
             @emptyEd="emptyEd"
-            :attrib="attrib"
           />
         </template>
       </div>
@@ -510,12 +510,12 @@
         :current-tree-i-d="currentTreeID"
         :config="config"
         :TaskModel="TaskModel"
+        :attrib="attrib"
         @handleWav="handleWav"
         @changePinyin="changePinyin"
         @changeEN="changeEN"
         @exitFullscreen="exitFullscreen"
         @changeIsFull="changeIsFull"
-        :attrib="attrib"
       />
     </div>
   </div>
@@ -849,7 +849,7 @@ export default {
       }
       let pos = time.indexOf(':');
       let min = 0;
-      var sec = 0;
+      let sec = 0;
       if (pos > 0) {
         min = parseInt(time.substring(0, pos));
         sec = parseFloat(time.substring(pos + 1));

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

@@ -22,10 +22,10 @@
         >
           <transition-group>
             <div
-              class="option_one"
-              :id="item.id"
               v-for="(item, i) in data.structure_select_list"
+              :id="item.id"
               :key="'op' + i"
+              class="option_one"
               :style="{
                 background:
                   data.unified_attrib && data.unified_attrib.topic_color ? data.unified_attrib.topic_color : '',

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

@@ -13,8 +13,8 @@
         :width="audio_width"
         :ed="ed"
         type="audioLine"
-        @emptyEd="emptyEd"
         :attrib="data.unified_attrib"
+        @emptyEd="emptyEd"
       />
     </template>
     <div

+ 26 - 26
src/views/book/courseware/preview/components/new_word/NewWordPreview.vue

@@ -14,7 +14,7 @@
         >
           <div class="NPC-top-left">
             <span class="NPC-topTitle-text" v-html="data.title_con"></span>
-            <span class="NPC-topTitle-text" v-if="showLang">
+            <span v-if="showLang" class="NPC-topTitle-text">
               {{ titleTrans[getLang()] }}
             </span>
           </div>
@@ -40,8 +40,8 @@
                   :get-cur-time="getCurTime"
                   :ed="ed"
                   type="audioLine"
-                  @handleListenRead="handleListenRead"
                   :attrib="data.unified_attrib"
+                  @handleListenRead="handleListenRead"
                 />
               </div>
               <ul
@@ -73,7 +73,6 @@
                       ></a> -->
                       <SvgIcon
                         v-if="curTime >= sItem.bg && curTime < sItem.ed && stopAudioS"
-                        @click="handleChangeTime(sItem.bg, sItem.ed)"
                         icon-class="animated"
                         size="24"
                         :style="{
@@ -82,10 +81,10 @@
                               ? data.unified_attrib.topic_color
                               : '',
                         }"
+                        @click="handleChangeTime(sItem.bg, sItem.ed)"
                       />
                       <SvgIcon
                         v-else
-                        @click="handleChangeTime(sItem.bg, sItem.ed)"
                         icon-class="play-btn"
                         :style="{
                           color:
@@ -93,6 +92,7 @@
                               ? data.unified_attrib.topic_color
                               : '',
                         }"
+                        @click="handleChangeTime(sItem.bg, sItem.ed)"
                       />
                     </template>
                     <template v-else-if="sItem.mp3_list">
@@ -189,14 +189,14 @@
                         >
                         </span>
                         <span
-                          class="NPC-word-tab-common"
-                          :style="{ width: data.col_width[0].value + 'px' }"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][0]
                           "
+                          class="NPC-word-tab-common"
+                          :style="{ width: data.col_width[0].value + 'px' }"
                         >
                           {{ multilingualTextList[getLang()][index][0] }}
                         </span>
@@ -208,13 +208,13 @@
                           v-html="sItem.cixing"
                         ></p>
                         <span
-                          class="NPC-word-tab-common"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][1]
                           "
+                          class="NPC-word-tab-common"
                         >
                           {{ multilingualTextList[getLang()][index][1] }}
                         </span>
@@ -222,13 +222,13 @@
                       <span :style="{ width: data.col_width[3].value + 'px' }">
                         <p class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></p>
                         <span
-                          class="NPC-word-tab-common"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][2]
                           "
+                          class="NPC-word-tab-common"
                         >
                           {{ multilingualTextList[getLang()][index][2] }}
                         </span>
@@ -259,21 +259,21 @@
                       >
                         <p
                           class="NPC-word-tab-common NPC-word-tab-word"
-                          v-html="sItem.new_word"
                           :style="{
                             fontSize:
                               data.unified_attrib && data.unified_attrib.font_size ? data.unified_attrib.font_size : '',
                           }"
+                          v-html="sItem.new_word"
                         ></p>
                         <span
-                          class="NPC-word-tab-common"
-                          :style="{ width: data.col_width[0].value + 'px' }"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][0]
                           "
+                          class="NPC-word-tab-common"
+                          :style="{ width: data.col_width[0].value + 'px' }"
                         >
                           {{ multilingualTextList[getLang()][index][0] }}
                         </span>
@@ -303,13 +303,13 @@
                         ></p>
 
                         <span
-                          class="NPC-word-tab-common"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][1]
                           "
+                          class="NPC-word-tab-common"
                         >
                           {{ multilingualTextList[getLang()][index][1] }}
                         </span>
@@ -317,13 +317,13 @@
                       <span :style="{ width: data.col_width[3].value + 'px' }">
                         <p class="NPC-word-tab-common NPC-word-tab-def" v-html="sItem.def_str"></p>
                         <span
-                          class="NPC-word-tab-common"
                           v-if="
                             showLang &&
                             multilingualTextList[getLang()] &&
                             multilingualTextList[getLang()][index] &&
                             multilingualTextList[getLang()][index][2]
                           "
+                          class="NPC-word-tab-common"
                         >
                           {{ multilingualTextList[getLang()][index][2] }}
                         </span>
@@ -340,8 +340,8 @@
                           type="mini"
                           class="luyin-box-wordphrase"
                           :style="{ marginLeft: '8px' }"
-                          @handleWav="handleWav"
                           :attrib="data.unified_attrib"
+                          @handleWav="handleWav"
                         />
                       </span>
                       <span v-if="isEnable(data.property.is_has_infor)">
@@ -351,7 +351,7 @@
 
                     <div v-if="sItem.collocation" :style="{ width: data.col_width[4].value + 'px' }">
                       <span class="collocation"><span>搭配:</span><b v-html="sItem.collocation"></b></span>
-                      <span class="" v-if="showLang">
+                      <span v-if="showLang" class="">
                         {{
                           multilingualTextList[getLang()] &&
                           multilingualTextList[getLang()][index] &&
@@ -368,7 +368,7 @@
                           <b v-html="sItem.liju_list"></b>
                         </div>
                       </span>
-                      <span class="" v-if="showLang">
+                      <span v-if="showLang" class="">
                         {{
                           multilingualTextList[getLang()] &&
                           multilingualTextList[getLang()][index] &&
@@ -637,9 +637,9 @@ export default {
     AudioPlay,
     Strockplay,
   },
-  props: ['newData'],
   mixins: [PreviewMixin],
   inject: ['bookInfo'],
+  props: ['newData'],
   data() {
     return {
       data: this.newData ? this.newData : getNewWordData(),
@@ -689,6 +689,15 @@ export default {
       immediate: true,
     },
   },
+  mounted() {
+    this.width = `${
+      document.querySelector('.preview-main').offsetWidth -
+      200 -
+      20 -
+      (this.data.property.sn_display_mode === 'true' ? 15 : 0) -
+      (this.newData ? 16 : 0)
+    }px`;
+  },
   methods: {
     palyAudio(url, sIndex) {
       this.stopAudio();
@@ -903,15 +912,6 @@ export default {
       item.isFlipped = !item.isFlipped;
     },
   },
-  mounted() {
-    this.width =
-      document.querySelector('.preview-main').offsetWidth -
-      200 -
-      20 -
-      (this.data.property.sn_display_mode === 'true' ? 15 : 0) -
-      (this.newData ? 16 : 0) +
-      'px';
-  },
 };
 </script>