This commit is contained in:
@@ -22,6 +22,7 @@ const importPreview = ref<any>(null)
|
|||||||
const restoreFile = ref<File | null>(null)
|
const restoreFile = ref<File | null>(null)
|
||||||
const todayKey = ref(dateKey(new Date()))
|
const todayKey = ref(dateKey(new Date()))
|
||||||
const habitSwipeStart = ref<{ id: string; x: number; y: number } | null>(null)
|
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 habitSwipeOffsets = ref<Record<string, number>>({})
|
||||||
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
||||||
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
|
let dayRolloverTimer: ReturnType<typeof setInterval> | undefined
|
||||||
@@ -73,6 +74,25 @@ function startHabitSwipe(h: Habit, event: TouchEvent) {
|
|||||||
habitSwipeOffsets.value[h.id] = 0
|
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) {
|
function moveHabitSwipe(h: Habit, event: TouchEvent) {
|
||||||
const start = habitSwipeStart.value
|
const start = habitSwipeStart.value
|
||||||
const touch = event.touches[0]
|
const touch = event.touches[0]
|
||||||
@@ -121,6 +141,25 @@ async function finishHabitSwipe(h: Habit, event: TouchEvent) {
|
|||||||
await applyHabitSwipe(h, deltaX)
|
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) {
|
function cancelHabitSwipe(h?: Habit) {
|
||||||
habitSwipeStart.value = null
|
habitSwipeStart.value = null
|
||||||
if (h) habitSwipeOffsets.value[h.id] = 0
|
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">
|
<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>
|
<span class="swipe-bg" :class="{ cancel: isDone(h, todayKey) }">{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? '− 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}</span>
|
||||||
<div class="habit-main">
|
<div class="habit-main">
|
||||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
<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">
|
<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>
|
<span class="swipe-bg" :class="{ cancel: isDone(h, todayKey) }">{{ isDone(h, todayKey) ? (h.kind === 'numeric' ? '− 一次' : '↩ 未完成') : (h.kind === 'numeric' ? '+ 一次' : '✓ 打卡') }}</span>
|
||||||
<div class="habit-main">
|
<div class="habit-main">
|
||||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
<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 css = readFileSync('src/style.css', 'utf8')
|
||||||
const app = readFileSync('src/App.vue', 'utf8')
|
const app = readFileSync('src/App.vue', 'utf8')
|
||||||
|
const mvpPanel = readFileSync('src/MvpPanel.vue', 'utf8')
|
||||||
|
|
||||||
describe('mobile navigation styles', () => {
|
describe('mobile navigation styles', () => {
|
||||||
it('keeps the mobile More sheet visible when it is rendered', () => {
|
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)
|
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', () => {
|
it('does not draw a left border on task or habit rows', () => {
|
||||||
expect(css).toContain('.task-row{border-left:0;')
|
expect(css).toContain('.task-row{border-left:0;')
|
||||||
expect(css).toContain('.habit-row{border-left:0;')
|
expect(css).toContain('.habit-row{border-left:0;')
|
||||||
|
|||||||
Reference in New Issue
Block a user