|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
import { Activity, ArchiveRestore, Check, ChevronRight, Download, FileJson, GripVertical, LogOut, Pencil, Trash2, X } from 'lucide-vue-next'
|
|
|
|
|
import { moveItemWithinScope } from './lib/task-utils'
|
|
|
|
|
import { mergeReorderedSubset, moveItemWithinScope } from './lib/task-utils'
|
|
|
|
|
import { archivePanelFlags, changedHabitFields, dateKey, formatArchivedAt, formatAuditAction, formatAuditEntity, formatHabitApiError, formatLocalShortDateTime, formatUserAgent, habitActionState, habitButtonNotice, habitButtonValue, invalidateHabitGridCache, isHabitComplete, isHabitScheduledToday, mergePage, nextHabitSwipeValue, performHabitRestore, previousHabitSwipeValue, readHabitGridCache, shouldToggleRowSwipe, validateHabitForm, writeHabitGridCache, type ArchivePanelState, type HabitFormErrors, type HabitFormValues } from './lib/mvp-utils'
|
|
|
|
|
import { csrfHeader } from './lib/csrf'
|
|
|
|
|
import { createCompletionPulse, shouldAnimateCompletionExit, waitForCompletionExit } from './lib/completion-motion'
|
|
|
|
@@ -113,7 +113,7 @@ 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 || !props.showCompleted) return
|
|
|
|
|
if (busy.value) return
|
|
|
|
|
habitReorder.value = { id: h.id, startY: event.clientY, offsetY: 0 }
|
|
|
|
|
habitReorderTarget.value = h.id
|
|
|
|
|
try { (event.currentTarget as Element).setPointerCapture(event.pointerId) } catch { /* synthetic events */ }
|
|
|
|
@@ -136,12 +136,14 @@ async function finishHabitReorder(h: Habit, event: PointerEvent) {
|
|
|
|
|
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
|
|
|
|
|
const visiblePrevious = visibleHabits.value
|
|
|
|
|
const reorderedVisible = moveItemWithinScope(visiblePrevious, h.id, targetId, placement)
|
|
|
|
|
if (reorderedVisible === visiblePrevious) return
|
|
|
|
|
const next = mergeReorderedSubset(previous, reorderedVisible)
|
|
|
|
|
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) }) })
|
|
|
|
|
await request('/habits/reorder', { method: 'PUT', body: JSON.stringify({ habit_ids: reorderedVisible.map((item) => item.id) }) })
|
|
|
|
|
emit('notice', '顺序已保存')
|
|
|
|
|
} catch (e) {
|
|
|
|
|
habits.value = previous
|
|
|
|
@@ -632,7 +634,7 @@ onBeforeUnmount(() => {
|
|
|
|
|
<!-- 习惯列表支持整行滑动记录。 -->
|
|
|
|
|
<div v-if="view === 'habits'" class="habit-list">
|
|
|
|
|
<article v-for="h in visibleHabits" :key="h.id" :data-habit-id="h.id" class="habit-row swipeable" :class="{ done: isDone(h, todayKey), 'just-completed': justCompletedHabitIds.has(h.id), 'completion-exiting': completionExitingHabitIds.has(h.id), 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`, '--reorder-y': `${habitReorder?.id === h.id ? habitReorder.offsetY : 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="!showCompleted" 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="drag-handle habit-drag-handle" 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" :disabled="!habitAction(h).writable" :aria-disabled="!habitAction(h).writable" :title="habitAction(h).reason" :aria-label="habitAction(h).reason || (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" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
|
|
|
|
|
<span class="habit-name">{{ h.name }}</span>
|
|
|
|
|