refactor(V0.2): 拖动交互收紧 —— 仅把手触发 / 缩小把手 / 去多余元素 / 打卡跨组
Docker Build and Push / build-image (push) Successful in 1m3s
Docker Build and Push / build-image (push) Successful in 1m3s
- 打卡与列表: 去掉卡片整体长按拖动路径, 只有 ⠿ 把手可拖; 把手缩小 - 列表行去掉跟动的蓝色目标列徽标 (drag-pill), 占位/克隆浮层承担全部反馈 - 打卡支持跨组拖动: 落到其他周期组的目标卡 = 改周期 + 落位一次完成 (updateHabit period + reorder), 组内排序不变 - 看板: 去掉 ⠿ 把手, 恢复长按拖动
This commit is contained in:
@@ -62,12 +62,12 @@ export default function useHabitDrag({ onDrop }) {
|
|||||||
const el = document.elementFromPoint(x, y)
|
const el = document.elementFromPoint(x, y)
|
||||||
const rowEl = el?.closest?.('[data-habit-row]')
|
const rowEl = el?.closest?.('[data-habit-row]')
|
||||||
if (!rowEl) return null
|
if (!rowEl) return null
|
||||||
// 只认同一周期组的落点
|
|
||||||
if (rowEl.dataset.habitPeriod !== st.current.item?.period) return null
|
|
||||||
const r = rowEl.getBoundingClientRect()
|
const r = rowEl.getBoundingClientRect()
|
||||||
const id = Number(rowEl.dataset.habitId)
|
const id = Number(rowEl.dataset.habitId)
|
||||||
if (id === st.current.item.id) return null
|
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) => {
|
const onMove = (ev) => {
|
||||||
@@ -89,7 +89,7 @@ export default function useHabitDrag({ onDrop }) {
|
|||||||
const s = st.current
|
const s = st.current
|
||||||
if (s.active && s.item) {
|
if (s.active && s.item) {
|
||||||
lastEnd.current = Date.now()
|
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()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import StatusDot from './StatusDot.jsx'
|
|||||||
const MAX_SUB = 3
|
const MAX_SUB = 3
|
||||||
|
|
||||||
export default function TaskCard({
|
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 children = task.children || []
|
||||||
const shown = children.slice(0, MAX_SUB)
|
const shown = children.slice(0, MAX_SUB)
|
||||||
@@ -20,9 +20,6 @@ export default function TaskCard({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...(bindDrag ? bindDrag(task) : {})}
|
{...(bindDrag ? bindDrag(task) : {})}
|
||||||
>
|
>
|
||||||
{bindDragHandle && (
|
|
||||||
<span className="grip" title="拖动换列" {...bindDragHandle(task)}>⠿</span>
|
|
||||||
)}
|
|
||||||
<div className="brow1">
|
<div className="brow1">
|
||||||
<span className="btitle">{task.title}</span>
|
<span className="btitle">{task.title}</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const statusLabel = (s) => (s === 'done' ? '完成' : s === 'doing' ? '执行中
|
|||||||
// 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动,
|
// 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动,
|
||||||
// 避免长按行后抬手时合成的 click 落在控件上误触状态切换
|
// 避免长按行后抬手时合成的 click 落在控件上误触状态切换
|
||||||
export default function TaskRow({
|
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'
|
const done = task.status === 'done'
|
||||||
return (
|
return (
|
||||||
@@ -16,7 +16,7 @@ export default function TaskRow({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`${indent ? 'sub-row' : 'task-row'}${done ? ' done' : ''}`
|
`${indent ? 'sub-row' : 'task-row'}${done ? ' done' : ''}`
|
||||||
+ (dragTarget ? ' dragging' : '')
|
+ (dragging ? ' dragging' : '')
|
||||||
+ (dragOver ? ` dragover ${dragOver.after ? 'after' : 'before'}` : '')
|
+ (dragOver ? ` dragover ${dragOver.after ? 'after' : 'before'}` : '')
|
||||||
}
|
}
|
||||||
data-task-id={indent ? undefined : task.id}
|
data-task-id={indent ? undefined : task.id}
|
||||||
@@ -60,11 +60,6 @@ export default function TaskRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-right">
|
<div className="task-right">
|
||||||
{dragTarget && (
|
|
||||||
<span className={`drag-pill${dragTarget.id === task.columnId ? ' cur' : ''}`}>
|
|
||||||
→ {dragTarget.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span
|
<span
|
||||||
className="more"
|
className="more"
|
||||||
onClick={(e) => { e.stopPropagation(); onMore?.(task) }}
|
onClick={(e) => { e.stopPropagation(); onMore?.(task) }}
|
||||||
|
|||||||
@@ -100,18 +100,28 @@ export default function MergedView({ date, onDateChange }) {
|
|||||||
|
|
||||||
const drag = useHabitDrag({ onDrop: reorderHabit })
|
const drag = useHabitDrag({ onDrop: reorderHabit })
|
||||||
|
|
||||||
// 组内拖动落位: 重排后按渲染分组顺序整体展平重写 sortOrder
|
// 拖动落位: 组内排序; 落到其他周期组 = 同时改周期并落位。
|
||||||
async function reorderHabit(dragged, targetId, after) {
|
// 重排后按渲染分组顺序整体展平重写 sortOrder。
|
||||||
|
async function reorderHabit(dragged, targetId, after, targetPeriod) {
|
||||||
const snapshot = detail
|
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
|
if (!g) return
|
||||||
|
const target = g.items.find((i) => i.id === targetId)
|
||||||
const rest = g.items.filter((i) => i.id !== dragged.id)
|
const rest = g.items.filter((i) => i.id !== dragged.id)
|
||||||
const ti = rest.findIndex((i) => i.id === targetId)
|
const ti = rest.findIndex((i) => i.id === targetId)
|
||||||
if (ti < 0) return
|
if (ti < 0) return
|
||||||
rest.splice(after ? ti + 1 : ti, 0, dragged)
|
const crossed = targetPeriod && targetPeriod !== dragged.period
|
||||||
const flat = groups.map((x) => (x.key === g.key ? rest : x.items)).flat()
|
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 })) }))
|
setDetail((d) => ({ ...d, items: flat.map((it, i) => ({ ...it, sortOrder: i })) }))
|
||||||
try {
|
try {
|
||||||
|
if (crossed) await checkinApi.updateHabit(dragged.id, { period: targetPeriod })
|
||||||
await checkinApi.reorderHabits(flat.map((i) => i.id))
|
await checkinApi.reorderHabits(flat.map((i) => i.id))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setDetail(snapshot)
|
setDetail(snapshot)
|
||||||
@@ -324,7 +334,6 @@ export default function MergedView({ date, onDateChange }) {
|
|||||||
+ (drag.dragItem?.id === item.id ? ' dragging' : '')
|
+ (drag.dragItem?.id === item.id ? ' dragging' : '')
|
||||||
+ (drag.over?.id === item.id ? ` dragover ${drag.over.after ? 'after' : 'before'}` : '')
|
+ (drag.over?.id === item.id ? ` dragover ${drag.over.after ? 'after' : 'before'}` : '')
|
||||||
}
|
}
|
||||||
{...drag.bind(item)}
|
|
||||||
>
|
>
|
||||||
<span className="grip" title="拖动排序" {...drag.bindHandle(item)}>⠿</span>
|
<span className="grip" title="拖动排序" {...drag.bindHandle(item)}>⠿</span>
|
||||||
<RingProgress
|
<RingProgress
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export default function BoardView({
|
|||||||
task={t}
|
task={t}
|
||||||
dragging={drag.dragTask?.id === t.id}
|
dragging={drag.dragTask?.id === t.id}
|
||||||
bindDrag={drag.bind}
|
bindDrag={drag.bind}
|
||||||
bindDragHandle={drag.bindHandle}
|
|
||||||
onClick={() => { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }}
|
onClick={() => { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }}
|
||||||
onToggleSub={onToggleSub}
|
onToggleSub={onToggleSub}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
|||||||
@@ -52,9 +52,8 @@ export default function ListView({
|
|||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
onClick={() => { if (!drag.justEnded()) onOpen(t) }}
|
onClick={() => { if (!drag.justEnded()) onOpen(t) }}
|
||||||
onMore={onMore}
|
onMore={onMore}
|
||||||
bindDrag={drag.bind}
|
|
||||||
bindDragHandle={drag.bindHandle}
|
bindDragHandle={drag.bindHandle}
|
||||||
dragTarget={dragging ? orderedCols[dragging.toIdx] : null}
|
dragging={!!dragging}
|
||||||
dragOver={drag.over?.id === t.id ? drag.over : null}
|
dragOver={drag.over?.id === t.id ? drag.over : null}
|
||||||
/>
|
/>
|
||||||
{kids.length > 0 && (
|
{kids.length > 0 && (
|
||||||
|
|||||||
@@ -682,8 +682,8 @@ body.me-safe-top .app-safe-top { height: 0; }
|
|||||||
指针流不会被滚动抢占取消, 是 Android WebView 上唯一可靠的拖动入口 */
|
指针流不会被滚动抢占取消, 是 Android WebView 上唯一可靠的拖动入口 */
|
||||||
.grip {
|
.grip {
|
||||||
touch-action: none; -webkit-user-select: none; user-select: none;
|
touch-action: none; -webkit-user-select: none; user-select: none;
|
||||||
color: #c2c7cf; font-size: 14px; line-height: 1;
|
color: #c2c7cf; font-size: 12px; line-height: 1;
|
||||||
padding: 14px 6px; margin: -8px 0; min-width: 26px; min-height: 36px;
|
padding: 10px 3px; margin: -6px 0; min-width: 20px; min-height: 28px;
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
cursor: grab; flex-shrink: 0; align-self: center;
|
cursor: grab; flex-shrink: 0; align-self: center;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user