This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Activity, ArchiveRestore, Check, Download, FileJson, LogOut, RefreshCw, Trash2, Upload, X } from 'lucide-vue-next'
|
||||
import { Activity, ArchiveRestore, Check, Download, FileJson, GripVertical, LogOut, RefreshCw, Trash2, Upload, X } from 'lucide-vue-next'
|
||||
import { moveItemWithinScope } from './lib/task-utils'
|
||||
import { dateKey, habitButtonValue, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, previousHabitSwipeValue, readHabitGridCache, shouldToggleRowSwipe, writeHabitGridCache } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
|
||||
@@ -33,6 +34,8 @@ 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 habitReorder = ref<{ id: string; startY: number } | null>(null)
|
||||
const habitReorderTarget = ref('')
|
||||
const hideCompletedHabits = ref(false)
|
||||
const todayHabits = computed(() => habits.value.filter((item) => isHabitScheduledToday(item, todayKey.value)))
|
||||
const visibleTodayHabits = computed(() => hideCompletedHabits.value ? todayHabits.value.filter((item) => !isDone(item, todayKey.value)) : todayHabits.value)
|
||||
@@ -78,6 +81,43 @@ function isDone(h: Habit, day: string) { return isHabitComplete(h.kind, logFor(h
|
||||
function isInteractiveTarget(target: EventTarget | null) {
|
||||
return target instanceof Element && Boolean(target.closest('button,input,select,textarea,a,label'))
|
||||
}
|
||||
function startHabitReorder(h: Habit, event: PointerEvent) {
|
||||
if (busy.value || hideCompletedHabits.value) return
|
||||
habitReorder.value = { id: h.id, startY: event.clientY }
|
||||
habitReorderTarget.value = h.id
|
||||
try { (event.currentTarget as Element).setPointerCapture(event.pointerId) } catch { /* synthetic events */ }
|
||||
}
|
||||
function moveHabitReorder(h: Habit, event: PointerEvent) {
|
||||
if (habitReorder.value?.id !== h.id) return
|
||||
const row = document.elementFromPoint(event.clientX, event.clientY)?.closest<HTMLElement>('[data-habit-id]')
|
||||
if (row?.dataset.habitId) habitReorderTarget.value = row.dataset.habitId
|
||||
}
|
||||
async function finishHabitReorder(h: Habit, event: PointerEvent) {
|
||||
const drag = habitReorder.value
|
||||
const targetId = habitReorderTarget.value
|
||||
habitReorder.value = null
|
||||
habitReorderTarget.value = ''
|
||||
if (!drag || drag.id !== h.id || !targetId || targetId === h.id) return
|
||||
const placement = event.clientY >= drag.startY ? 'after' : 'before'
|
||||
const previous = habits.value
|
||||
const next = moveItemWithinScope(previous, h.id, targetId, placement)
|
||||
if (next === previous) return
|
||||
habits.value = next
|
||||
writeHabitGridCache(dateKey(new Date()), next)
|
||||
try {
|
||||
await request('/habits/reorder', { method: 'PUT', body: JSON.stringify({ habit_ids: next.map((item) => item.id) }) })
|
||||
emit('notice', '顺序已保存')
|
||||
} catch (e) {
|
||||
habits.value = previous
|
||||
writeHabitGridCache(dateKey(new Date()), previous)
|
||||
error.value = e instanceof Error ? e.message : '请求失败'
|
||||
}
|
||||
}
|
||||
function cancelHabitReorder() {
|
||||
habitReorder.value = null
|
||||
habitReorderTarget.value = ''
|
||||
}
|
||||
|
||||
function startHabitSwipe(h: Habit, event: TouchEvent) {
|
||||
if (busy.value || isInteractiveTarget(event.target)) return
|
||||
const touch = event.touches[0]
|
||||
@@ -346,7 +386,8 @@ onBeforeUnmount(() => {
|
||||
<!-- 今日习惯:只展示“今天该做”的习惯,复用正式习惯行样式 -->
|
||||
<div v-if="view === 'today-habits' && (habits.length || !busy)" class="habit-list today-habit-list">
|
||||
<div class="habit-toolbar habit-toolbar-today"><label><input v-model="hideCompletedHabits" type="checkbox"> 隐藏已完成</label></div>
|
||||
<article v-for="h in visibleTodayHabits" :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` }" @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)">
|
||||
<article v-for="h in visibleTodayHabits" :key="h.id" :data-habit-id="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64, reordering: habitReorder?.id === h.id, 'reorder-target': habitReorderTarget === h.id }" :style="{ '--swipe-x': `${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)">
|
||||
<button class="drag-handle habit-drag-handle" :disabled="hideCompletedHabits" aria-label="上下拖动习惯排序" title="上下拖动排序" @pointerdown.stop="startHabitReorder(h, $event)" @pointermove.stop="moveHabitReorder(h, $event)" @pointerup.stop="finishHabitReorder(h, $event)" @pointercancel.stop="cancelHabitReorder"><GripVertical/></button>
|
||||
<button class="task-check habit-check" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
|
||||
<div class="habit-main">
|
||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
||||
@@ -370,7 +411,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 习惯列表支持整行滑动记录。 -->
|
||||
<div v-if="view === 'habits'" class="habit-list">
|
||||
<article v-for="h in visibleHabits" :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` }" @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)">
|
||||
<article v-for="h in visibleHabits" :key="h.id" :data-habit-id="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), ready: Math.abs(habitSwipeOffsets[h.id] ?? 0) >= 64, reordering: habitReorder?.id === h.id, 'reorder-target': habitReorderTarget === h.id }" :style="{ '--swipe-x': `${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)">
|
||||
<button class="drag-handle habit-drag-handle" :disabled="hideCompletedHabits" aria-label="上下拖动习惯排序" title="上下拖动排序" @pointerdown.stop="startHabitReorder(h, $event)" @pointermove.stop="moveHabitReorder(h, $event)" @pointerup.stop="finishHabitReorder(h, $event)" @pointercancel.stop="cancelHabitReorder"><GripVertical/></button>
|
||||
<button class="task-check habit-check" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)"><span class="task-check-mark"><Check v-if="isDone(h, todayKey)" /></span></button>
|
||||
<div class="habit-main">
|
||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
||||
|
||||
Reference in New Issue
Block a user