diff --git a/src/components/checkin/useHabitDrag.js b/src/components/checkin/useHabitDrag.js index 1441710..0973069 100644 --- a/src/components/checkin/useHabitDrag.js +++ b/src/components/checkin/useHabitDrag.js @@ -18,6 +18,12 @@ export default function useHabitDrag({ onDrop }) { const st = useRef({ timer: null, active: false, startX: 0, startY: 0, item: null, over: null }) const lastEnd = useRef(0) + // onDrop 取"最新一次渲染"的闭包: bind/bindHandle 是空依赖 useCallback, + // 若直接捕获 onDrop, 会固化首次渲染(数据未加载, groups=[])的落位函数, + // 松手时静默早退 —— 顺序永远不变。用 ref 桥接到实时实现。 + const onDropRef = useRef(onDrop) + onDropRef.current = onDrop + const cleanup = useCallback(() => { if (st.current.timer) { clearTimeout(st.current.timer); st.current.timer = null } st.current.active = false @@ -61,17 +67,21 @@ export default function useHabitDrag({ onDrop }) { const s = st.current if (s.active && s.item) { lastEnd.current = Date.now() - if (s.over && s.over.id !== s.item.id) onDrop?.(s.item, s.over.id, s.over.after) + if (s.over && s.over.id !== s.item.id) onDropRef.current?.(s.item, s.over.id, s.over.after) } cleanup() } - // 指针监听装配: instant=true 时按下即激活 (把手路径), 否则长按 400ms 激活 + // 指针监听装配: instant=true 时按下即激活 (把手路径), 否则长按 400ms 激活。 + // 把手路径额外挂非 passive touchstart preventDefault —— 规范上这会禁止 + // 浏览器对"整个触摸"的一切默认处理 (含滚动), 比 touch-action:none 更强, + // 双保险防滚动抢占取消指针流。 const armListeners = (ev, instant, item) => { try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ } const el = ev.currentTarget const move = (e2) => onMove(e2) const tm = (e2) => { if (st.current.active) e2.preventDefault() } + const ts = (e2) => e2.preventDefault() const up = () => { onUp(); detach() } const cancel = () => { if (st.current.active) lastEnd.current = Date.now() @@ -83,11 +93,13 @@ export default function useHabitDrag({ onDrop }) { el.removeEventListener('pointerup', up) el.removeEventListener('pointercancel', cancel) el.removeEventListener('touchmove', tm) + el.removeEventListener('touchstart', ts) } el.addEventListener('pointermove', move) el.addEventListener('pointerup', up) el.addEventListener('pointercancel', cancel) el.addEventListener('touchmove', tm, { passive: false }) + if (instant) el.addEventListener('touchstart', ts, { passive: false }) if (instant) { st.current.active = true diff --git a/src/components/task/useRowDrag.js b/src/components/task/useRowDrag.js index acd25ef..47588b1 100644 --- a/src/components/task/useRowDrag.js +++ b/src/components/task/useRowDrag.js @@ -59,12 +59,15 @@ export default function useRowDrag({ columns, onDrop }) { cleanup() } - // 指针监听装配 (capture + 非 passive touchmove: 激活后尽力拦截滚动接管) - const armListeners = (ev) => { + // 指针监听装配 (capture + 非 passive touchmove: 激活后尽力拦截滚动接管; + // instant=true 把手路径再挂 touchstart preventDefault, 规范上禁止浏览器对 + // 整个触摸的默认处理, 彻底杜绝滚动抢占) + const armListeners = (ev, instant = false) => { try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ } const el = ev.currentTarget const move = (e2) => onMove(e2) const tm = (e2) => { if (st.current.active) e2.preventDefault() } + const ts = (e2) => e2.preventDefault() const up = () => { onUp(); detach() } const cancel = () => { if (st.current.active) lastEnd.current = Date.now() @@ -76,11 +79,13 @@ export default function useRowDrag({ columns, onDrop }) { el.removeEventListener('pointerup', up) el.removeEventListener('pointercancel', cancel) el.removeEventListener('touchmove', tm) + el.removeEventListener('touchstart', ts) } el.addEventListener('pointermove', move) el.addEventListener('pointerup', up) el.addEventListener('pointercancel', cancel) el.addEventListener('touchmove', tm, { passive: false }) + if (instant) el.addEventListener('touchstart', ts, { passive: false }) } /** 绑定到列表行: 行元素需设置 touch-action: pan-y (长按路径, 可滚场景不可靠) */ @@ -115,7 +120,7 @@ export default function useRowDrag({ columns, onDrop }) { st.current.active = true if (navigator.vibrate) navigator.vibrate(15) setDrag({ task, fromIdx, toIdx: fromIdx }) - armListeners(ev) + armListeners(ev, true) }, onContextMenu: (ev) => ev.preventDefault(), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/styles/globals.css b/src/styles/globals.css index ffa1b5f..6586ffe 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -682,7 +682,9 @@ body.me-safe-top .app-safe-top { height: 0; } 指针流不会被滚动抢占取消, 是 Android WebView 上唯一可靠的拖动入口 */ .grip { touch-action: none; -webkit-user-select: none; user-select: none; - color: #c2c7cf; font-size: 14px; line-height: 1; padding: 8px 2px; + color: #c2c7cf; font-size: 14px; line-height: 1; + padding: 14px 6px; margin: -8px 0; min-width: 26px; min-height: 36px; + display: inline-flex; align-items: center; justify-content: center; cursor: grab; flex-shrink: 0; align-self: center; } .grip:active { color: var(--color-primary); }