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;
}