diff --git a/src/components/checkin/useHabitDrag.js b/src/components/checkin/useHabitDrag.js index a2f5c00..e21a1b2 100644 --- a/src/components/checkin/useHabitDrag.js +++ b/src/components/checkin/useHabitDrag.js @@ -62,12 +62,12 @@ export default function useHabitDrag({ onDrop }) { const el = document.elementFromPoint(x, y) const rowEl = el?.closest?.('[data-habit-row]') if (!rowEl) return null - // 只认同一周期组的落点 - if (rowEl.dataset.habitPeriod !== st.current.item?.period) return null const r = rowEl.getBoundingClientRect() const id = Number(rowEl.dataset.habitId) if (id === st.current.item.id) return null - return { id, after: y > r.top + r.height / 2 } + // 允许跨组: 落到其他周期组的目标卡上 = 同时改周期 + 落位 (由 onDrop 处理) + const period = rowEl.dataset.habitPeriod + return { id, period, after: y > r.top + r.height / 2 } } const onMove = (ev) => { @@ -89,7 +89,7 @@ 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) onDropRef.current?.(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, s.over.period) } cleanup() } diff --git a/src/components/task/TaskCard.jsx b/src/components/task/TaskCard.jsx index dec07c7..c7ed88a 100644 --- a/src/components/task/TaskCard.jsx +++ b/src/components/task/TaskCard.jsx @@ -7,7 +7,7 @@ import StatusDot from './StatusDot.jsx' const MAX_SUB = 3 export default function TaskCard({ - task, bindDrag, bindDragHandle, onClick, onToggleSub, onToggle, onMore, dragging = false, highlight = false, + task, bindDrag, onClick, onToggleSub, onToggle, onMore, dragging = false, highlight = false, }) { const children = task.children || [] const shown = children.slice(0, MAX_SUB) @@ -20,9 +20,6 @@ export default function TaskCard({ onClick={onClick} {...(bindDrag ? bindDrag(task) : {})} > - {bindDragHandle && ( - ⠿ - )}
{task.title} (s === 'done' ? '完成' : s === 'doing' ? '执行中 // 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动, // 避免长按行后抬手时合成的 click 落在控件上误触状态切换 export default function TaskRow({ - task, onToggle, onClick, onMore, bindDrag, bindDragHandle, dragTarget, dragOver, indent = false, children, + task, onToggle, onClick, onMore, bindDrag, bindDragHandle, dragging = false, dragOver, indent = false, children, }) { const done = task.status === 'done' return ( @@ -16,7 +16,7 @@ export default function TaskRow({
- {dragTarget && ( - - → {dragTarget.name} - - )} { e.stopPropagation(); onMore?.(task) }} diff --git a/src/pages/checkin/MergedView.jsx b/src/pages/checkin/MergedView.jsx index 9424394..60a09ee 100644 --- a/src/pages/checkin/MergedView.jsx +++ b/src/pages/checkin/MergedView.jsx @@ -100,18 +100,28 @@ export default function MergedView({ date, onDateChange }) { const drag = useHabitDrag({ onDrop: reorderHabit }) - // 组内拖动落位: 重排后按渲染分组顺序整体展平重写 sortOrder - async function reorderHabit(dragged, targetId, after) { + // 拖动落位: 组内排序; 落到其他周期组 = 同时改周期并落位。 + // 重排后按渲染分组顺序整体展平重写 sortOrder。 + async function reorderHabit(dragged, targetId, after, targetPeriod) { const snapshot = detail - const g = groups.find((x) => x.key === (GROUP_ORDER.includes(dragged.period) ? dragged.period : 'other')) + const g = groups.find((x) => x.items.some((i) => i.id === targetId)) if (!g) return + const target = g.items.find((i) => i.id === targetId) const rest = g.items.filter((i) => i.id !== dragged.id) const ti = rest.findIndex((i) => i.id === targetId) if (ti < 0) return - rest.splice(after ? ti + 1 : ti, 0, dragged) - const flat = groups.map((x) => (x.key === g.key ? rest : x.items)).flat() + const crossed = targetPeriod && targetPeriod !== dragged.period + const moved = crossed + ? { ...dragged, period: targetPeriod, periodLabel: GROUP_LABEL[targetPeriod] || target.periodLabel } + : dragged + rest.splice(after ? ti + 1 : ti, 0, moved) + // 展平时把拖动项从原组剔除 (跨组场景原组可能仍包含它) + const flat = groups + .map((x) => (x.key === g.key ? rest : x.items.filter((i) => i.id !== dragged.id))) + .flat() setDetail((d) => ({ ...d, items: flat.map((it, i) => ({ ...it, sortOrder: i })) })) try { + if (crossed) await checkinApi.updateHabit(dragged.id, { period: targetPeriod }) await checkinApi.reorderHabits(flat.map((i) => i.id)) } catch (e) { setDetail(snapshot) @@ -324,7 +334,6 @@ export default function MergedView({ date, onDateChange }) { + (drag.dragItem?.id === item.id ? ' dragging' : '') + (drag.over?.id === item.id ? ` dragover ${drag.over.after ? 'after' : 'before'}` : '') } - {...drag.bind(item)} > ⠿ { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }} onToggleSub={onToggleSub} onToggle={onToggle} diff --git a/src/pages/task/ListView.jsx b/src/pages/task/ListView.jsx index 932516c..326abff 100644 --- a/src/pages/task/ListView.jsx +++ b/src/pages/task/ListView.jsx @@ -52,9 +52,8 @@ export default function ListView({ onToggle={onToggle} onClick={() => { if (!drag.justEnded()) onOpen(t) }} onMore={onMore} - bindDrag={drag.bind} bindDragHandle={drag.bindHandle} - dragTarget={dragging ? orderedCols[dragging.toIdx] : null} + dragging={!!dragging} dragOver={drag.over?.id === t.id ? drag.over : null} /> {kids.length > 0 && ( diff --git a/src/styles/globals.css b/src/styles/globals.css index 90ab25c..c512973 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -682,8 +682,8 @@ 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: 14px 6px; margin: -8px 0; min-width: 26px; min-height: 36px; + color: #c2c7cf; font-size: 12px; line-height: 1; + padding: 10px 3px; margin: -6px 0; min-width: 20px; min-height: 28px; display: inline-flex; align-items: center; justify-content: center; cursor: grab; flex-shrink: 0; align-self: center; }