Przeglądaj źródła

feat(SmartDialog): body 任意位置可拖动外层弹窗 (5px 阈值 + 100ms 视觉提示 + 元素豁免)

- SmartDialog: 新增 onBodyMousedown 阈值豁免机制
  - 豁免 button/input/select/textarea/a/[contenteditable]/[tabindex]/
    .drag-handle/.resize-handle/.close-btn/[data-no-drag], 不影响原交互
  - 用户按住超 5px 触发外层窗口拖动 (阈值内 click 正常透传)
  - 按住 100ms 后 isPendingDrag=true, cursor 强制变 grabbing 作为视觉提示
  - 通过 isPendingDrag/isDragging 状态切换 + CSS !important 覆盖子组件 cursor
- CrossingMultiView: .cell-close 加 data-no-drag (关闭按钮不触发拖动)
- 与 vuedraggable 兼容: 三个点 ⠇ 换位手柄通过 .drag-handle 豁免清单与新机制共存
画安 2 tygodni temu
rodzic
commit
d218caca40

+ 2 - 2
src/components/ui/CrossingMultiView.vue

@@ -36,7 +36,7 @@
                             :intersectionData="slot.headerData ? slot.headerData.intersectionData : {}"
                             :cycleLength="slot.headerData ? slot.headerData.cycleLength : 0"
                         />
-                        <span class="cell-close" @click.stop="handleRemove(slot.data.id)">✕</span>
+                        <span class="cell-close" @click.stop="handleRemove(slot.data.id)" data-no-drag>✕</span>
                     </div>
                     <div class="cell-body">
                         <CrossingDetailPanel
@@ -307,7 +307,7 @@ export default {
     padding: 6px 10px 10px 10px;
 }
 
-/* ===== 拖拽把手 ===== */
+/* ===== 拖拽把手 (vuedraggable 换位手柄 ⠇; SmartDialog 已豁免, 不会触发外层窗口拖动) ===== */
 .drag-handle {
     display: flex;
     align-items: center;

+ 64 - 4
src/components/ui/SmartDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <div v-show="visible" class="smart-dialog" :style="dialogStyle" @mousedown="onRootMousedown" @dblclick="handleDoubleClick" :class="{ 'no-padding': noPadding }">
+  <div v-show="visible" class="smart-dialog" :style="dialogStyle" @mousedown="onRootMousedown" @dblclick="handleDoubleClick" :class="{ 'no-padding': noPadding, 'is-pending-drag': isPendingDrag, 'is-dragging': isDragging }">
     <div class="dialog-header" :class="{ 'is-draggable': draggable }" @mousedown="startDrag" v-if="title">
       <div class="title-content">
         <slot name="header">
@@ -12,7 +12,7 @@
 
     <div v-if="title" class="dialog-divider"></div>
 
-    <div class="dialog-body" :class="{ 'no-padding': noPadding }">
+    <div class="dialog-body" :class="{ 'no-padding': noPadding }" @mousedown="onBodyMousedown">
       <slot></slot>
     </div>
 
@@ -57,9 +57,10 @@ export default {
       y: 0,
       w: 0,
       h: 0,
-      currentScale: 1, 
+      currentScale: 1,
       zIndex: globalZIndex,
       isDragging: false,
+      isPendingDrag: false,   // 用户按住 100ms 后, 视觉上提示"准备拖动"
       isResizing: false,
       dragOffset: { x: 0, y: 0 },
       resizeStart: { x: 0, y: 0, w: 0, h: 0 }
@@ -67,13 +68,18 @@ export default {
   },
   computed: {
     dialogStyle() {
+      // 拖动相关状态优先 → 显示 grabbing 表示"正在拖"
+      let cursor;
+      if (this.isDragging || this.isPendingDrag) cursor = 'grabbing';
+      else if (this.enableDblclickExpand) cursor = 'pointer';
+      else cursor = 'default';
       return {
         left: `${this.x}px`,
         top: `${this.y}px`,
         width: `${this.w}px`,
         height: `${this.h}px`,
         zIndex: this.zIndex,
-        cursor: this.enableDblclickExpand ? 'pointer' : 'default'
+        cursor,
       };
     }
   },
@@ -181,6 +187,52 @@ export default {
     onRootMousedown() {
       if (this.bringToFrontOnMousedown) this.bringToFront();
     },
+
+    /** body 区域 mousedown 入口: "阈值 + 豁免"机制, 让用户在内容区任意位置按住拖动也能移动外层弹窗
+     *  - 豁免清单内的元素 (button/input/拖动手柄等) → 直接放行, 不影响原交互
+     *  - 其它位置 → 启动 5px 阈值监听; 100ms 后 cursor 变 grabbing 提示
+     *    - 阈值内松开 → 原生 click 正常触发 (按钮 click 不丢)
+     *    - 超阈值 → 调用 startDrag 移动外层弹窗 */
+    onBodyMousedown(e) {
+      if (!this.draggable) return;
+
+      // 豁免清单: 原生交互元素 / vuedraggable handle / SmartDialog 自身控件 / 业务声明
+      const EXEMPT = 'button, input, select, textarea, a, [contenteditable], [tabindex],'
+        + ' .drag-handle, .resize-handle, .close-btn, [data-no-drag]';
+      if (e.target.closest(EXEMPT)) return;
+
+      const startX = e.clientX, startY = e.clientY;
+      const THRESHOLD = 5;
+      const HINT_DELAY = 100;   // 用户按住超过 100ms 给视觉提示
+      let dragStarted = false;
+
+      const hintTimer = setTimeout(() => {
+        if (!dragStarted) this.isPendingDrag = true;
+      }, HINT_DELAY);
+
+      const onMove = (ev) => {
+        if (dragStarted) return;
+        const dx = Math.abs(ev.clientX - startX);
+        const dy = Math.abs(ev.clientY - startY);
+        if (dx > THRESHOLD || dy > THRESHOLD) {
+          dragStarted = true;
+          clearTimeout(hintTimer);
+          this.isPendingDrag = false;
+          // 用初始 mousedown 位置触发拖动, 保证 dragOffset 准确
+          this.startDrag({ clientX: startX, clientY: startY, target: e.target });
+        }
+      };
+
+      const onUp = () => {
+        clearTimeout(hintTimer);
+        this.isPendingDrag = false;
+        document.removeEventListener('mousemove', onMove);
+        document.removeEventListener('mouseup', onUp);
+      };
+
+      document.addEventListener('mousemove', onMove);
+      document.addEventListener('mouseup', onUp);
+    },
     
     calculatePosition() {
       this.$nextTick(() => {
@@ -294,6 +346,14 @@ export default {
 
 <style scoped>
 /* =========== CSS 保持不变 =========== */
+/* 拖动中/即将拖动时, 强制整个弹窗 (含子组件) 都显示 grabbing 光标作为视觉提示 */
+.smart-dialog.is-pending-drag,
+.smart-dialog.is-pending-drag *,
+.smart-dialog.is-dragging,
+.smart-dialog.is-dragging * {
+  cursor: grabbing !important;
+}
+
 .smart-dialog {
   position: fixed;
   background: radial-gradient(circle at 20% 0%, rgba(40,120,200,0.2) 0%, rgba(20,60,130,0.4) 70%);