This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
const css = readFileSync('src/style.css', 'utf8')
|
||||
const app = readFileSync('src/App.vue', 'utf8')
|
||||
const mvpPanel = readFileSync('src/MvpPanel.vue', 'utf8')
|
||||
|
||||
describe('mobile navigation styles', () => {
|
||||
it('keeps the mobile More sheet visible when it is rendered', () => {
|
||||
@@ -20,6 +21,13 @@ describe('task and habit row decoration', () => {
|
||||
expect(app.match(/@pointercancel="cancelTaskPointer/g)?.length).toBe(2)
|
||||
})
|
||||
|
||||
it('supports pointer dragging for desktop habit rows in Today and Habits views', () => {
|
||||
expect(mvpPanel.match(/@pointerdown="startHabitPointer/g)?.length).toBe(2)
|
||||
expect(mvpPanel.match(/@pointermove="moveHabitPointer/g)?.length).toBe(2)
|
||||
expect(mvpPanel.match(/@pointerup="finishHabitPointer/g)?.length).toBe(2)
|
||||
expect(mvpPanel.match(/@pointercancel="cancelHabitPointer/g)?.length).toBe(2)
|
||||
})
|
||||
|
||||
it('does not draw a left border on task or habit rows', () => {
|
||||
expect(css).toContain('.task-row{border-left:0;')
|
||||
expect(css).toContain('.habit-row{border-left:0;')
|
||||
|
||||
Reference in New Issue
Block a user