From 83cc218c5e9bd24eff64312db4008a8c550fe34f Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sun, 25 Oct 2026 01:42:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(V0.2):=20=E6=8B=96=E5=8A=A8=E8=B7=9F?= =?UTF-8?q?=E6=89=8B=E6=B5=AE=E5=B1=82=20=E2=80=94=E2=80=94=20=E5=8D=8A?= =?UTF-8?q?=E9=80=8F=E6=98=8E=E5=89=AF=E6=9C=AC=E9=9A=8F=E6=8C=87=E9=92=88?= =?UTF-8?q?,=20=E5=8E=9F=E4=BD=8D=E8=99=9A=E7=BA=BF=E5=8D=A0=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useRowDrag/useHabitDrag 暴露 ghostRef, 拖动激活与移动时直接改 transform (绕过 React 重渲染, 保证跟手流畅) - 打卡浮层带颜色点 + 名称; 任务列表浮层带任务名 + 实时目标列名 - 原位卡片降为低透明度虚线占位, 落点指示不变 --- src/components/checkin/useHabitDrag.js | 12 +++++++++++- src/components/task/useRowDrag.js | 11 ++++++++++- src/pages/checkin/MergedView.jsx | 8 ++++++++ src/pages/task/ListView.jsx | 13 +++++++++++-- src/styles/globals.css | 15 ++++++++++++++- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/components/checkin/useHabitDrag.js b/src/components/checkin/useHabitDrag.js index 0973069..3d4df57 100644 --- a/src/components/checkin/useHabitDrag.js +++ b/src/components/checkin/useHabitDrag.js @@ -24,6 +24,13 @@ export default function useHabitDrag({ onDrop }) { const onDropRef = useRef(onDrop) onDropRef.current = onDrop + // 跟手浮层: 消费方渲染
, 拖动时 hook 直接改 + // transform (绕过 React 重渲染, 保证跟手流畅) + const ghostRef = useRef(null) + const moveGhost = (x, y) => { + if (ghostRef.current) ghostRef.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -110%)` + } + const cleanup = useCallback(() => { if (st.current.timer) { clearTimeout(st.current.timer); st.current.timer = null } st.current.active = false @@ -58,6 +65,7 @@ export default function useHabitDrag({ onDrop }) { return } ev.preventDefault() + moveGhost(ev.clientX, ev.clientY) const hit = hitRow(ev.clientX, ev.clientY) if (hit) { s.over = hit; setOver(hit) } else if (s.over) { s.over = null; setOver(null) } @@ -104,11 +112,13 @@ export default function useHabitDrag({ onDrop }) { if (instant) { st.current.active = true setDragItem(item) + moveGhost(st.current.startX, st.current.startY) if (navigator.vibrate) navigator.vibrate(15) } else { st.current.timer = setTimeout(() => { st.current.active = true setDragItem(item) + moveGhost(st.current.startX, st.current.startY) if (navigator.vibrate) navigator.vibrate(15) }, 400) } @@ -137,5 +147,5 @@ export default function useHabitDrag({ onDrop }) { // eslint-disable-next-line react-hooks/exhaustive-deps }), []) - return { dragItem, over, bind, bindHandle, cancel: cleanup, justEnded } + return { dragItem, over, bind, bindHandle, ghostRef, cancel: cleanup, justEnded } } diff --git a/src/components/task/useRowDrag.js b/src/components/task/useRowDrag.js index 47588b1..06c368e 100644 --- a/src/components/task/useRowDrag.js +++ b/src/components/task/useRowDrag.js @@ -33,6 +33,12 @@ export default function useRowDrag({ columns, onDrop }) { /** 拖动刚结束 (350ms 内): 抑制松手合成的 click 误开详情页 */ const justEnded = useCallback(() => Date.now() - lastEnd.current < 350, []) + // 跟手浮层: 消费方渲染
, 拖动时直接改 transform + const ghostRef = useRef(null) + const moveGhost = (x, y) => { + if (ghostRef.current) ghostRef.current.style.transform = `translate(${x}px, ${y}px) translate(-50%, -110%)` + } + const onMove = (ev) => { const s = st.current if (!s.active) { @@ -42,6 +48,7 @@ export default function useRowDrag({ columns, onDrop }) { return } ev.preventDefault() + moveGhost(ev.clientX, ev.clientY) const to = Math.max(0, Math.min(ordered.length - 1, s.fromIdx + Math.round((ev.clientX - s.startX) / STEP))) if (to !== s.toIdx) { s.toIdx = to @@ -99,6 +106,7 @@ export default function useRowDrag({ columns, onDrop }) { st.current.active = true if (navigator.vibrate) navigator.vibrate(15) setDrag({ task, fromIdx, toIdx: fromIdx }) + moveGhost(st.current.startX, st.current.startY) }, 400) armListeners(ev) }, @@ -120,11 +128,12 @@ export default function useRowDrag({ columns, onDrop }) { st.current.active = true if (navigator.vibrate) navigator.vibrate(15) setDrag({ task, fromIdx, toIdx: fromIdx }) + moveGhost(st.current.startX, st.current.startY) armListeners(ev, true) }, onContextMenu: (ev) => ev.preventDefault(), // eslint-disable-next-line react-hooks/exhaustive-deps }), [ordered, onDrop]) - return { drag, bind, bindHandle, cancel: cleanup, justEnded } + return { drag, bind, bindHandle, ghostRef, cancel: cleanup, justEnded } } diff --git a/src/pages/checkin/MergedView.jsx b/src/pages/checkin/MergedView.jsx index ed25826..f0d4372 100644 --- a/src/pages/checkin/MergedView.jsx +++ b/src/pages/checkin/MergedView.jsx @@ -363,6 +363,14 @@ export default function MergedView({ date, onDateChange }) { ))}
+ {/* 跟手浮层: 半透明副本随手指移动, 原位保留虚线占位 */} + {drag.dragItem && ( +
+ + {drag.dragItem.name} +
+ )} + {/* 「⋯」操作面板: 撤销 / 编辑 / 归档 */} - {groups.map((g) => ( -
+ {groups.map((g) => (
{g.name} {g.list.length} @@ -74,6 +73,16 @@ export default function ListView({ })}
))} + + {/* 跟手浮层: 任务名 + 目标列, 原位保留半透明占位 */} + {drag.drag && ( +
+ {drag.drag.task.title} + {orderedCols[drag.drag.toIdx] && ( + → {orderedCols[drag.drag.toIdx].name} + )} +
+ )}
) } diff --git a/src/styles/globals.css b/src/styles/globals.css index 6586ffe..de96294 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -694,11 +694,24 @@ body.me-safe-top .app-safe-top { height: 0; } .mv-view { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } .mv-view input, .mv-view textarea { -webkit-user-select: text; user-select: text; } .mv-card { touch-action: pan-y; } -.mv-card.dragging { opacity: .55; outline: 2px dashed var(--color-primary); outline-offset: 1px; } +.mv-card.dragging { opacity: .3; outline: 2px dashed var(--color-primary); outline-offset: 1px; } .mv-card.dragover { outline: 2px solid var(--color-primary); outline-offset: 1px; } .mv-card.dragover.after { border-bottom: 3px solid var(--color-primary); } .mv-card.dragover.before { border-top: 3px solid var(--color-primary); } +/* 拖动跟手浮层: 半透明副本随指针, 原位保留占位 */ +.drag-ghost { + position: fixed; left: 0; top: 0; z-index: 1000; + display: flex; align-items: center; gap: 6px; + background: rgba(255,255,255,.94); color: var(--color-text); + font-size: 13px; font-weight: 600; white-space: nowrap; + padding: 8px 12px; border-radius: 8px; + box-shadow: 0 10px 24px rgba(15,23,42,.28); + pointer-events: none; will-change: transform; +} +.drag-ghost i { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.drag-ghost b { color: var(--color-primary); font-weight: 600; margin-left: 4px; } + /* ----- 日历视图 ----- */ .calendar-view { padding: 0 12px 20px; } .cal-month-bar { display: flex; align-items: center; gap: 10px; padding: 8px 2px; }