Bladeren bron

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

zq 1 dag geleden
bovenliggende
commit
4683efe0ae

BIN
src/assets/icon-publish.png


+ 56 - 4
src/components/CommonPreview.vue

@@ -15,14 +15,24 @@
     </div>
 
     <div class="audit-content">
-      <div class="main-container">
+      <div ref="previewMain" class="main-container">
         <main :class="['preview-main', { 'no-audit': !isShowAudit }]">
           <span class="title">
             <SvgIcon icon-class="menu-2" size="24" />
             <span>{{ courseware_info.name_path }}</span>
           </span>
-
-          <CoursewarePreview :data="data" :component-list="component_list" :background="background" />
+          <CoursewarePreview
+            v-if="courseware_info.book_name"
+            ref="courserware"
+            :data="data"
+            :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="isShowAudit"
+            :component-remark-obj="remark_list_obj"
+            @computeScroll="computeScroll"
+            @addRemark="addRemark"
+          />
         </main>
       </div>
       <div v-if="isShowAudit" class="remark-list">
@@ -70,6 +80,7 @@
 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/common';
 
 import {
   GetBookCoursewareInfo,
@@ -137,9 +148,16 @@ export default {
       data: { row_list: [] },
       component_list: [],
       remark_list: [],
+      remark_list_obj: {}, // 存放以组件为对象的数组
       visible: false,
       remark_content: '',
       submit_loading: false,
+      isTrue,
+      menuPosition: {
+        x: -1,
+        y: -1,
+        componentId: 'WHOLE',
+      },
     };
   },
   created() {
@@ -217,15 +235,38 @@ export default {
     // 审校批注列表
     getCoursewareAuditRemarkList(id) {
       this.remark_list = [];
+      let remarkListObj = {};
       GetCoursewareAuditRemarkList({
         courseware_id: id,
       }).then(({ remark_list }) => {
         this.remark_list = remark_list;
+        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;
       });
     },
-    addRemark() {
+    addRemark(selectNode, x, y, componentId) {
       this.remark_content = '';
       this.visible = true;
+      if (selectNode) {
+        this.menuPosition = {
+          x,
+          y,
+          componentId,
+        };
+      } else {
+        this.menuPosition = {
+          x: -1,
+          y: -1,
+          componentId: 'WHOLE',
+        };
+      }
     },
     dialogClose() {
       this.visible = false;
@@ -236,6 +277,9 @@ export default {
       AddCoursewareAuditRemark({
         courseware_id: id || this.id,
         content: this.remark_content,
+        component_id: this.menuPosition.componentId,
+        position_x: this.menuPosition.x,
+        position_y: this.menuPosition.y,
       })
         .then(() => {
           this.submit_loading = false;
@@ -261,6 +305,14 @@ export default {
         })
         .catch(() => {});
     },
+    // 计算previewMain滑动距离
+    computeScroll() {
+      this.$refs.courserware.handleResult(
+        this.$refs.previewMain.scrollTop,
+        this.$refs.previewMain.scrollLeft,
+        this.select_node,
+      );
+    },
   },
 };
 </script>

+ 1 - 0
src/icons/svg/icon-info.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750763248804" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4816" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M606.8 895.7H214c-47.9 0-86.8-38.9-86.8-86.8V215c0-47.9 38.8-86.8 86.7-86.8h593.9c23-0.1 45.1 9.1 61.4 25.4 16.4 16.4 25.3 38.2 25.4 61.4l-1.4 392.8v0.1c0.2 17.7 14.6 31.9 32.3 31.7 17.4-0.2 31.5-14.2 31.7-31.6l1.3-392.9v-0.1c0-83.2-67.4-150.7-150.6-150.7H214c-83.1-0.1-150.6 67.3-150.6 150.4V808.9c-0.1 83.1 67.2 150.6 150.3 150.7h393.2c17.6 0 31.9-14.3 31.9-31.9 0-17.7-14.3-32-32-32z" p-id="4817"></path><path d="M949.3 905l-122-122c61.5-86.4 41.3-206.4-45.2-267.9s-206.4-41.3-267.9 45.2c-61.5 86.4-41.3 206.4 45.2 267.9 66.7 47.4 156 47.4 222.7 0l122 122c12.5 12.5 32.7 12.5 45.2 0s12.5-32.7 0-45.2zM671 799.8c-70.7 0-127.9-57.3-127.9-127.9s57.2-128 127.9-128 127.9 57.3 127.9 127.9c0 33.9-13.5 66.5-37.5 90.5-23.9 24.1-56.5 37.6-90.4 37.5z m95.9-447.5c17.6 0 31.9-14.4 31.9-32s-14.3-32.1-31.9-32.1l-511.6-0.2c-17.7 0-31.9 14.3-31.9 32 0 17.6 14.3 32.1 31.9 32.1l511.6 0.2zM447.1 544c17.6 0 31.9-14.3 31.9-31.9 0-17.6-14.3-31.9-31.9-31.9H255.3c-17.6 0-31.9 14.3-31.9 31.9s14.3 31.9 31.9 31.9h191.8zM255.3 671.8c-17.6 0-31.9 14.3-31.9 31.9 0 17.6 14.3 31.9 31.9 31.9h127.9c17.6 0.2 32.1-14 32.3-31.6 0.2-17.6-14-32.1-31.6-32.3h-0.7l-127.9 0.1z" p-id="4818"></path></svg>

+ 1 - 0
src/icons/svg/icon-publish.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750756332954" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6075" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M879.542857 780.251429h-73.142857v-73.142858a27.428571 27.428571 0 1 0-54.857143 0v73.142858h-73.142857a27.428571 27.428571 0 0 0 0 54.857142h73.142857v73.142858a27.428571 27.428571 0 0 0 54.857143 0v-73.142858h73.142857a27.428571 27.428571 0 1 0 0-54.857142zM894.354286 351.634286a36.571429 36.571429 0 0 0-2.011429-49.737143L687.725714 98.742857a36.571429 36.571429 0 0 0-49.371428-2.011428L317.805714 368.822857l-69.485714-3.108571a37.668571 37.668571 0 0 0-27.611429 10.605714L194.194286 402.285714a36.571429 36.571429 0 0 0 0 51.931429l144.091428 143.177143L128 806.948571A36.571429 36.571429 0 0 0 128 859.428571a36.571429 36.571429 0 0 0 25.965714 10.788572A36.571429 36.571429 0 0 0 179.382857 859.428571l210.834286-210.102857 144.457143 143.725715a36.571429 36.571429 0 0 0 51.565714 0l26.697143-26.514286a36.571429 36.571429 0 0 0 10.788571-27.794286l-3.291428-68.754286z m-338.834286 281.6A36.571429 36.571429 0 0 0 546.742857 658.285714l1.828572 45.714286-132.754286-132.022857-132.571429-131.474286 45.897143 2.011429a37.668571 37.668571 0 0 0 25.417143-8.594286L660.114286 174.262857 816.64 329.142857z" fill="#040000" p-id="6076"></path></svg>

+ 4 - 0
src/views/book/courseware/create/components/question/math/Math.vue

@@ -101,6 +101,10 @@ export default {
       }
       return `$\\${macro}$`;
     },
+    /**
+     * 插入宏到公式中
+     * @param {string} macro - 宏字符串
+     */
     insertMacro(macro) {
       let _macro = macro.trim();
       let math = this.data.math || '';

+ 130 - 11
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -1,5 +1,6 @@
 <template>
   <div
+    ref="courserware"
     class="courserware"
     :style="[
       {
@@ -20,17 +21,51 @@
           <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
             <!-- 网格 -->
             <template v-for="(grid, k) in col.grid_list">
-              <component
-                :is="previewComponentList[grid.type]"
-                ref="preview"
-                :key="k"
-                :content="computedColContent(grid.id)"
-                :class="[grid.id]"
-                :style="{
-                  gridArea: grid.grid_area,
-                  height: grid.height,
-                }"
-              />
+              <div :key="k" @contextmenu.prevent="handleContextMenu($event, grid.id)">
+                <component
+                  :is="previewComponentList[grid.type]"
+                  ref="preview"
+                  :content="computedColContent(grid.id)"
+                  :class="[grid.id]"
+                  :style="{
+                    gridArea: grid.grid_area,
+                    height: grid.height,
+                  }"
+                />
+              </div>
+
+              <div
+                v-if="showMenu && componentId === grid.id"
+                :key="'menu' + grid.id + k"
+                class="custom-context-menu"
+                :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }"
+                @click="handleMenuItemClick"
+              >
+                添加批注
+              </div>
+              <div
+                v-if="showRemark && Object.keys(componentRemarkObj).length !== 0 && componentRemarkObj[grid.id]"
+                :key="'show' + grid.id + k"
+              >
+                <el-popover
+                  v-for="(items, indexs) in componentRemarkObj[grid.id]"
+                  :key="indexs"
+                  placement="bottom"
+                  width="200"
+                  trigger="click"
+                >
+                  <div v-html="items.content"></div>
+                  <template #reference>
+                    <SvgIcon
+                      slot="reference"
+                      icon-class="icon-info"
+                      size="24"
+                      class="remark-info"
+                      :style="{ left: items.position_x - 12 + 'px', top: items.position_y - 12 + 'px' }"
+                    />
+                  </template>
+                </el-popover>
+              </div>
             </template>
           </div>
         </template>
@@ -63,6 +98,18 @@ export default {
       type: Array,
       required: true,
     },
+    canRemark: {
+      type: Boolean,
+      default: false,
+    },
+    showRemark: {
+      type: Boolean,
+      default: false,
+    },
+    componentRemarkObj: {
+      type: Object,
+      default: () => ({}),
+    },
   },
   data() {
     return {
@@ -70,7 +117,26 @@ export default {
       bookInfo: {
         theme_color: '',
       },
+      showMenu: false,
+      divPosition: {
+        left: 0,
+        top: 0,
+      }, // courserware盒子原始距离页面顶部和左边的距离
+      menuPosition: { x: 0, y: 0, select_node: '' }, // 用于存储菜单的位置
+      componentId: '', // 添加批注的组件id
+    };
+  },
+  mounted() {
+    const element = this.$refs.courserware;
+    const rect = element.getBoundingClientRect();
+    this.divPosition = {
+      left: rect.left,
+      top: rect.top,
     };
+    window.addEventListener('mousedown', this.handleMouseDown);
+  },
+  beforeDestroy() {
+    window.removeEventListener('mousedown', this.handleMouseDown);
   },
   methods: {
     /**
@@ -199,12 +265,48 @@ export default {
         gridTemplateRows,
       };
     },
+    handleContextMenu(event, id) {
+      if (this.canRemark) {
+        event.preventDefault(); // 阻止默认的上下文菜单显示
+        this.menuPosition = {
+          x: event.clientX - this.divPosition.left,
+          y: event.clientY - this.divPosition.top,
+        }; // 设置菜单位置
+        this.componentId = id;
+        this.$emit('computeScroll');
+      }
+    },
+    handleResult(top, left, select_node) {
+      this.menuPosition = {
+        x: this.menuPosition.x + left,
+        y: this.menuPosition.y + top,
+        select_node,
+      }; // 设置菜单位置
+      this.showMenu = true; // 显示菜单
+    },
+    handleMenuItemClick() {
+      this.showMenu = false; // 隐藏菜单
+      this.$emit(
+        'addRemark',
+        this.menuPosition.select_node,
+        this.menuPosition.x,
+        this.menuPosition.y,
+        this.componentId,
+      );
+    },
+    handleMouseDown(event) {
+      if (event.button === 0 && event.target.className !== 'custom-context-menu') {
+        // 0 表示左键
+        this.showMenu = false;
+      }
+    },
   },
 };
 </script>
 
 <style lang="scss" scoped>
 .courserware {
+  position: relative;
   display: flex;
   flex-direction: column;
   row-gap: 6px;
@@ -227,5 +329,22 @@ export default {
       overflow: hidden;
     }
   }
+
+  .custom-context-menu,
+  .remark-info {
+    position: absolute;
+    z-index: 999;
+    display: flex;
+    gap: 3px;
+    align-items: center;
+    font-size: 14px;
+    cursor: pointer;
+  }
+
+  .custom-context-menu {
+    padding-left: 30px;
+    background: url("../../../../assets/icon-publish.png") left center no-repeat;
+    background-size: 24px;
+  }
 }
 </style>

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

@@ -27,7 +27,7 @@
     >
       <div
         v-for="(itemP, indexP) in data.text_list"
-        :key="indexP"
+        :key="'text' + indexP"
         :class="['position-item', mageazineDetailIndex === indexP ? 'active' : '']"
         :style="{
           width: itemP.width,
@@ -39,7 +39,7 @@
       ></div>
       <div
         v-for="(itemP, indexP) in data.input_list"
-        :key="indexP"
+        :key="'input' + indexP"
         :class="['position-item position-item-input', 'active']"
         :style="{
           width: itemP.width,

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

@@ -363,7 +363,7 @@ export default {
       ];
       const operable = event.composedPath().some((item) => {
         const className = item.className;
-        if (!className) return false;
+        if (!className || typeof className !== 'string') return false;
         return whitePath.some((path) => className.includes(path));
       });
       if (!operable) {

+ 20 - 2
src/views/personal_workbench/project/ProductionResourceManage.vue

@@ -69,8 +69,21 @@
               <video controls :src="item.file_url" width="100%" height="140px"></video>
             </template>
 
-            <p class="name">{{ item.name }}</p>
-            <b class="label">{{ item.label }}</b>
+            <el-popover placement="bottom" width="300" trigger="hover">
+              <div class="sources-info">
+                <p class="name">名称:{{ item.name }}</p>
+                <p class="label">标签:{{ item.label }}</p>
+                <p class="name">简介:{{ item.intro }}</p>
+                <p class="label">修改时间:{{ item.last_modify_time }}</p>
+                <p class="label">文件大小:{{ item.file_size_desc }}</p>
+              </div>
+              <template #reference>
+                <div class="sources-info">
+                  <p class="name">{{ item.name }}</p>
+                  <b class="label">{{ item.label }}</b>
+                </div>
+              </template>
+            </el-popover>
           </div>
         </div>
         <PaginationPage ref="pagination" :total="total" @getList="queryList" />
@@ -199,6 +212,10 @@ export default {
           value: 4,
           label: '3D 模型',
         },
+        {
+          value: 5,
+          label: '文本',
+        },
       ], // 类型分类
       type_index: 0, // 类型索引
       sort_list: [
@@ -635,6 +652,7 @@ export default {
         .label {
           margin: 5px 0;
           font-size: 14px;
+          font-weight: normal;
           line-height: 1.3;
         }