This commit is contained in:
@@ -6,18 +6,12 @@ import { csrfHeader } from './lib/csrf'
|
||||
|
||||
type View = 'habits' | 'today-habits' | 'settings'
|
||||
type Habit = { id: string; name: string; kind?: string; target?: number; max_value?: number | null; unit?: string; cells?: Array<{ day: string; scheduled?:boolean; paused?:boolean; value: number | boolean }>; stats?: Record<string, number> }
|
||||
type CalendarSubscription = { id: string; name: string; url: string; color: string; enabled?: boolean }
|
||||
type CalendarEvent = { id: string; title: string; starts_at: string; ends_at?: string | null; all_day: boolean; source_name: string; color?: string }
|
||||
type Session = { id: string; created_at?: string; last_seen_at?: string; current?: boolean; user_agent?: string }
|
||||
const props = defineProps<{ view: View }>()
|
||||
const emit = defineEmits<{ changed: []; notice: [message: string] }>()
|
||||
const habits = ref<Habit[]>([])
|
||||
const sessions = ref<Session[]>([])
|
||||
const audit = ref<any[]>([])
|
||||
const calendarSubscriptions = ref<CalendarSubscription[]>([])
|
||||
const todayEvents = ref<CalendarEvent[]>([])
|
||||
const calendarName = ref('合并日历')
|
||||
const calendarUrl = ref('https://cal.bboy.app/all.ics')
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const habitName = ref('')
|
||||
@@ -68,16 +62,6 @@ async function safe(work: () => Promise<void>) {
|
||||
|
||||
function logFor(h: Habit, day: string) { return (h.cells ?? []).find((c) => c.day === day) }
|
||||
function isDone(h: Habit, day: string) { return isHabitComplete(h.kind, logFor(h, day)?.value, h.target ?? 1) }
|
||||
async function toggleHabit(h: Habit, day: string) {
|
||||
if (busy.value) return
|
||||
const current = logFor(h, day)
|
||||
const next = current?.value ? 0 : 1
|
||||
await safe(async () => {
|
||||
await request(`/habits/${h.id}/logs/${day}`, { method: 'PUT', body: JSON.stringify({ value: next }) })
|
||||
await loadHabits()
|
||||
emit('notice', next ? '打卡成功 🎉' : '已取消打卡')
|
||||
})
|
||||
}
|
||||
function isInteractiveTarget(target: EventTarget | null) {
|
||||
return target instanceof Element && Boolean(target.closest('button,input,select,textarea,a,label'))
|
||||
}
|
||||
@@ -105,13 +89,7 @@ function moveHabitSwipe(h: Habit, event: TouchEvent) {
|
||||
}
|
||||
}
|
||||
function habitProgressText(h: Habit) {
|
||||
if (h.kind !== 'numeric') return isDone(h, todayKey.value) ? '今天已完成 · 左滑取消' : '右滑整行打卡'
|
||||
const value = Number(logFor(h, todayKey.value)?.value ?? 0)
|
||||
return `${value} / ${h.target ?? 1} · ${isDone(h, todayKey.value) ? '左滑减少一次' : '右滑完成一次'}`
|
||||
}
|
||||
|
||||
function habitActionText(h: Habit) {
|
||||
if (h.kind !== 'numeric') return isDone(h, todayKey.value) ? '已完成' : '打卡'
|
||||
if (h.kind !== 'numeric') return ''
|
||||
return `${Number(logFor(h, todayKey.value)?.value ?? 0)} / ${h.target ?? 1}`
|
||||
}
|
||||
|
||||
@@ -195,50 +173,6 @@ async function loadSettings() {
|
||||
audit.value = mergePage<any>(a).items
|
||||
})
|
||||
}
|
||||
async function loadCalendarTab() {
|
||||
await safe(async () => {
|
||||
const [subs] = await Promise.all([request('/calendar-subscriptions').catch(() => [])])
|
||||
calendarSubscriptions.value = (subs as CalendarSubscription[]) ?? []
|
||||
await loadTodayEvents()
|
||||
})
|
||||
}
|
||||
async function addCalendarSubscription() {
|
||||
if (!calendarUrl.value.trim()) return
|
||||
await safe(async () => {
|
||||
await request('/calendar-subscriptions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: calendarName.value.trim() || calendarUrl.value, url: calendarUrl.value.trim(), color: '#f15a29' }),
|
||||
})
|
||||
await loadCalendarTab()
|
||||
emit('notice', '日历已订阅')
|
||||
})
|
||||
}
|
||||
async function removeCalendarSubscription(id: string) {
|
||||
if (!confirm('取消这个日历订阅?')) return
|
||||
await safe(async () => {
|
||||
await request(`/calendar-subscriptions/${id}`, { method: 'DELETE' })
|
||||
await loadCalendarTab()
|
||||
emit('notice', '已取消订阅')
|
||||
})
|
||||
}
|
||||
function habitTimeText(h: Habit) {
|
||||
return isDone(h, todayKey.value) ? '✓ 已完成' : '未完成'
|
||||
}
|
||||
async function loadTodayEvents() {
|
||||
if (!calendarSubscriptions.value.length) {
|
||||
todayEvents.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const events = await request(`/calendar-subscriptions/today?day=${todayKey.value}`) as CalendarEvent[]
|
||||
todayEvents.value = events ?? []
|
||||
} catch { todayEvents.value = [] }
|
||||
}
|
||||
function formatEventTime(item: CalendarEvent) {
|
||||
if (item.all_day) return '全天'
|
||||
const date = new Date(item.starts_at)
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
async function revoke(id: string) {
|
||||
await safe(async () => { await request(`/sessions/${id}`, { method: 'DELETE' }); await loadSettings(); emit('notice', '会话已撤销') })
|
||||
}
|
||||
@@ -300,8 +234,8 @@ onBeforeUnmount(() => {
|
||||
<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)">
|
||||
<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>{{ habitProgressText(h) }}</small></span>
|
||||
<button class="habit-swipe-hint" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" @click.stop="toggleHabitFromButton(h)">{{ habitActionText(h) }}</button>
|
||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
||||
<button class="sr-only" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" @click.stop="toggleHabitFromButton(h)">切换习惯状态</button>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!todayHabits.length" class="empty-panel">今天没有安排习惯,轻松一下吧。</div>
|
||||
@@ -320,13 +254,13 @@ onBeforeUnmount(() => {
|
||||
</form>
|
||||
|
||||
|
||||
<!-- 习惯列表:布尔习惯整行右滑打卡,数值习惯保留明确输入记录 -->
|
||||
<!-- 习惯列表支持整行滑动记录。 -->
|
||||
<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)">
|
||||
<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>{{ habitProgressText(h) }}</small></span>
|
||||
<button class="habit-swipe-hint" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)">{{ habitActionText(h) }}</button>
|
||||
<span><span class="habit-name">{{ h.name }}</span><small v-if="habitProgressText(h)">{{ habitProgressText(h) }}</small></span>
|
||||
<button class="sr-only" :aria-label="isDone(h, todayKey) ? `减少${h.name}一次` : `完成${h.name}一次`" :aria-pressed="isDone(h, todayKey)" @click.stop="toggleHabitFromButton(h)">切换习惯状态</button>
|
||||
</div>
|
||||
<button class="icon ghost" aria-label="归档习惯" @click="archiveHabit(h)"><Trash2 /></button>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user