Przeglądaj źródła

优化 getGridElementByPoint

dsy 3 tygodni temu
rodzic
commit
1b423c5dcb

+ 82 - 17
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -104,6 +104,7 @@
         height: `${menuPosition.endY - menuPosition.startY}px`,
         border: '2px solid #165DFF',
         zIndex: '9',
+        pointerEvents: 'none',
       }"
     ></div>
     <!-- 选中文本的工具栏 -->
@@ -272,6 +273,7 @@ export default {
       curSelectId: '', // 当前选中组件id
 
       isSelecting: false, // 是否开始框选内容
+      selectionRect: null, // 框选开始时缓存的课件位置(视口坐标),供整段框选复用
       file_preview_url: getConfig() ? getConfig().doc_preview_service_address : '',
       visible: false,
       newpath: '',
@@ -1092,22 +1094,42 @@ export default {
         });
       }
     },
+    /**
+     * 把鼠标事件的视口坐标换算成相对课件左上角的坐标,并裁剪到课件范围内
+     * @param {MouseEvent} event 鼠标事件
+     * @return {{ x: number, y: number }} 相对课件左上角、且已裁剪到课件盒子内的坐标
+     */
+    getPointInCourseware(event) {
+      // 整段框选复用 mousedown 时缓存的 rect:既避免 mousemove 反复触发同步重排,
+      // 也保证起止坐标基于同一份坐标系(反之若中途滚动/重排,落点会整体偏移)
+      const rect = this.selectionRect || this.$el.getBoundingClientRect();
+      const x = event.clientX - rect.left;
+      const y = event.clientY - rect.top;
+      // 边界校验:拖出课件的部分按课件边界处理,避免产生负数或超出课件的坐标
+      return {
+        x: Math.min(Math.max(x, 0), rect.width),
+        y: Math.min(Math.max(y, 0), rect.height),
+      };
+    },
     startSelection(event) {
-      if (this.canRemark) {
-        this.isSelecting = true;
-        let clientRect = document.getElementById(`selectable-area-preview`).getBoundingClientRect();
-        this.menuPosition.startX = event.clientX - clientRect.left;
-        this.menuPosition.startY = event.clientY - clientRect.top;
-        this.menuPosition.endX = null;
-        this.menuPosition.endY = null;
-      }
+      if (!this.canRemark) return;
+
+      this.isSelecting = true;
+      const { left, top, width, height } = this.$el.getBoundingClientRect();
+      this.selectionRect = { left, top, width, height };
+
+      const { x, y } = this.getPointInCourseware(event);
+      this.menuPosition.startX = x;
+      this.menuPosition.startY = y;
+      this.menuPosition.endX = null;
+      this.menuPosition.endY = null;
     },
     updateSelection(event) {
       if (!this.isSelecting || !this.canRemark) return;
-      let clientRect = document.getElementById(`selectable-area-preview`).getBoundingClientRect();
 
-      this.menuPosition.endX = event.clientX - clientRect.left;
-      this.menuPosition.endY = event.clientY - clientRect.top;
+      const { x, y } = this.getPointInCourseware(event);
+      this.menuPosition.endX = x;
+      this.menuPosition.endY = y;
     },
     endSelection() {
       this.isSelecting = false;
@@ -1153,20 +1175,63 @@ export default {
       this.visible = true;
     },
     /**
+     * 判断元素是否为本课件的一级网格组件(.courseware > .row > .col > .grid[data-id])
+     * 用于把命中范围严格限制在当前课件内部,避免命中内嵌课件或页面上的其它课件
+     * @param {Element} element 待判断的元素
+     * @return {boolean} 是否为本课件的一级网格组件
+     */
+    isOwnGridElement(element) {
+      const courseware = this.$el;
+      if (!courseware || !element) return false;
+      // 边界校验 1:元素必须位于当前课件内部
+      if (!courseware.contains(element)) return false;
+      // 边界校验 2:必须是带 data-id 的网格组件
+      if (!element.classList.contains('grid') || !element.dataset.id) return false;
+      // 边界校验 3:网格所在的 .row 必须是课件根节点的直接子节点,
+      // 借此排除内嵌课件(如 MobileCoursewarePreview)里结构相同的网格
+      const row = element.closest('.row');
+      return Boolean(row && row.parentElement === courseware);
+    },
+    /**
      * 根据相对 .courseware 元素左上角的坐标,获取命中的组件(.grid)元素
      * @param {number} x 相对 .courseware 元素左上角的 x 坐标
      * @param {number} y 相对 .courseware 元素左上角的 y 坐标
      * @return {HTMLElement|null} 命中的 .grid 元素,如果没有找到则返回 null
      */
     getGridElementByPoint(x, y) {
-      const coursewareRect = this.$el.getBoundingClientRect();
-      let el = document.elementFromPoint(coursewareRect.left + x, coursewareRect.top + y);
-      // 向上查找,直到找到具有 data-id 属性和 grid 类的元素
-      while (el && (!el.dataset.id || !el.classList.contains('grid'))) {
-        el = el.parentElement;
+      const courseware = this.$el;
+      if (!courseware) return null;
+
+      // 边界校验:坐标必须是有效数字,且落在课件盒子内
+      // (溢出到课件左侧的行复选框、或框选拖出课件都会产生负值/越界值)
+      const coursewareRect = courseware.getBoundingClientRect();
+      if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
+      if (x < 0 || y < 0 || x > coursewareRect.width || y > coursewareRect.height) return null;
+
+      const pointX = coursewareRect.left + x;
+      const pointY = coursewareRect.top + y;
+
+      // 用 elementsFromPoint(复数)拿到该点自顶向下的所有元素:
+      // 框选框、批注高亮层、工具栏等覆盖层会挡住 elementFromPoint(单数),
+      // 但它们在 elementsFromPoint 的结果里只是最上层的几项,跳过它们即可命中下方真正的组件
+      const stack =
+        typeof document.elementsFromPoint === 'function'
+          ? document.elementsFromPoint(pointX, pointY)
+          : [document.elementFromPoint(pointX, pointY)];
+
+      for (const element of stack) {
+        // 边界校验:只在该点属于当前课件内部时才开始上溯
+        if (!element || !courseware.contains(element)) continue;
+
+        // 自下而上查找,最多上溯到课件根节点为止,绝不越界到课件之外
+        let node = element;
+        while (node && node !== courseware) {
+          if (this.isOwnGridElement(node)) return node;
+          node = node.parentElement;
+        }
       }
 
-      return el || null;
+      return null;
     },
     /**
      * 根据相对 .courseware 元素左上角的坐标获取组件id

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

@@ -614,6 +614,7 @@ export default {
     }
 
     .fill-content {
+      width: calc(100% - 82px);
       margin: 0;
 
       :deep .el-input__inner {

+ 0 - 1
src/views/personal_workbench/check_task/audit/index.vue

@@ -112,7 +112,6 @@ export default {
 .audit {
   @include page-content(true);
 
-  overflow: hidden;
   user-select: none;
 
   &__header {