feat: support desktop habit swipe toggles
ci / docker (push) Successful in 3m27s

This commit is contained in:
2026-09-07 11:25:29 +08:00
parent e1137ca861
commit 373c038fe7
2 changed files with 49 additions and 2 deletions
+41 -2
View File
@@ -22,6 +22,7 @@ const importPreview = ref<any>(null)
const restoreFile = ref<File | null>(null)
const todayKey = ref(dateKey(new Date()))
const habitSwipeStart = ref<{ id: string; x: number; y: number } | null>(null)
const habitPointerStart = ref<{ id: string; x: number; y: number } | null>(null)
const habitSwipeOffsets = ref<Record<string, number>>({})
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
@@ -73,6 +74,25 @@ function startHabitSwipe(h: Habit, event: TouchEvent) {
habitSwipeOffsets.value[h.id] = 0
}
}
function startHabitPointer(h: Habit, event: PointerEvent) {
if (busy.value || isInteractiveTarget(event.target)) return
if (event.pointerType === 'touch') return
habitPointerStart.value = { id: h.id, x: event.clientX, y: event.clientY }
}
function moveHabitPointer(h: Habit, event: PointerEvent) {
const start = habitPointerStart.value
if (!start || start.id !== h.id) return
const deltaX = event.clientX - start.x
const deltaY = event.clientY - start.y
const current = Number(logFor(h, todayKey.value)?.value ?? 0)
if (Math.abs(deltaX) > Math.abs(deltaY)) {
const canIncrement = deltaX > 0 && current < (h.target ?? 1)
const canDecrement = deltaX < 0 && current > 0
if (canIncrement || canDecrement) {
habitSwipeOffsets.value[h.id] = Math.max(-92, Math.min(deltaX, 92))
}
}
}
function moveHabitSwipe(h: Habit, event: TouchEvent) {
const start = habitSwipeStart.value
const touch = event.touches[0]
@@ -121,6 +141,25 @@ async function finishHabitSwipe(h: Habit, event: TouchEvent) {
await applyHabitSwipe(h, deltaX)
}
}
async function finishHabitPointer(h: Habit, event: PointerEvent) {
const start = habitPointerStart.value
if (!start || start.id !== h.id) return
habitPointerStart.value = null
habitSwipeOffsets.value[h.id] = 0
const deltaX = event.clientX - start.x
const deltaY = event.clientY - start.y
const current = Number(logFor(h, todayKey.value)?.value ?? 0)
const expectedDirection = h.kind === 'numeric'
? ((deltaX > 0 && current < (h.target ?? 1)) || (deltaX < 0 && current > 0))
: (isDone(h, todayKey.value) ? deltaX < 0 : deltaX > 0)
if (expectedDirection && shouldToggleRowSwipe(Math.abs(deltaX), deltaY)) {
void applyHabitSwipe(h, deltaX)
}
}
function cancelHabitPointer(h?: Habit) {
habitPointerStart.value = null
if (h) habitSwipeOffsets.value[h.id] = 0
}
function cancelHabitSwipe(h?: Habit) {
habitSwipeStart.value = null
if (h) habitSwipeOffsets.value[h.id] = 0
@@ -231,7 +270,7 @@ onBeforeUnmount(() => {
<!-- 今日习惯只展示今天该做的习惯复用正式习惯行样式 -->
<div v-if="view === 'today-habits' && !busy" class="habit-list today-habit-list">
<article v-for="h in todayHabits" :key="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--swipe-width': `${Math.abs(habitSwipeOffsets[h.id] ?? 0)}px` }" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<article v-for="h in todayHabits" :key="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--swipe-width': `${Math.abs(habitSwipeOffsets[h.id] ?? 0)}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<span class="swipe-bg" :class="{ cancel: isDone(h, todayKey) }">{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? ' 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}</span>
<div class="habit-main">
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
@@ -256,7 +295,7 @@ onBeforeUnmount(() => {
<!-- 习惯列表支持整行滑动记录 -->
<div v-if="view === 'habits'" class="habit-list">
<article v-for="h in habits" :key="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--swipe-width': `${Math.abs(habitSwipeOffsets[h.id] ?? 0)}px` }" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<article v-for="h in habits" :key="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64 }" :style="{ '--swipe-x': `${habitSwipeOffsets[h.id] ?? 0}px`, '--swipe-width': `${Math.abs(habitSwipeOffsets[h.id] ?? 0)}px` }" @pointerdown="startHabitPointer(h, $event)" @pointermove="moveHabitPointer(h, $event)" @pointerup="finishHabitPointer(h, $event)" @pointercancel="cancelHabitPointer(h)" @touchstart.passive="startHabitSwipe(h, $event)" @touchmove.passive="moveHabitSwipe(h, $event)" @touchend="finishHabitSwipe(h, $event)" @touchcancel="cancelHabitSwipe(h)">
<span class="swipe-bg" :class="{ cancel: isDone(h, todayKey) }">{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? ' 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}</span>
<div class="habit-main">
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>