feat: add current view refresh action
ci / gitleaks (push) Successful in 8s
ci / docker (push) Successful in 3m32s

This commit is contained in:
2026-09-11 11:02:15 +08:00
parent 8b475e4df5
commit b59adf446b
4 changed files with 34 additions and 7 deletions
+17 -2
View File
@@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { import {
ArchiveRestore, CalendarDays, CalendarHeart, Check, ChevronDown, ChevronRight, Folder, ArchiveRestore, CalendarDays, CalendarHeart, Check, ChevronDown, ChevronRight, Folder,
Ellipsis, GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search, Ellipsis, GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search,
Settings, Trash2, X, Repeat2, Settings, Trash2, X, Repeat2, RefreshCw,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { buildTaskRecurrencePayload, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRecurrence, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig, type TaskRepeatOption } from './lib/task-utils' import { buildTaskRecurrencePayload, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRecurrence, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig, type TaskRepeatOption } from './lib/task-utils'
import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, normalizeRequiredName, performTrashMutation, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils' import { beginLatestRequest, createMutationReconciler, formatApiErrorDetail, isLatestRequest, isTaskView, loadCountdownCache, nextTotalAfterLocalTaskAdd, nextTotalAfterLocalTaskRemoval, normalizeRequiredName, performTrashMutation, readStoredBoolean, readStoredNavigation, runLatestRequest, shouldToggleRowSwipe, startPrimaryWithBackground, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
@@ -79,6 +79,7 @@ const searchRevealStyle = computed(() => ({
const error = ref('') const error = ref('')
const notice = ref('') const notice = ref('')
const loading = ref(false) const loading = ref(false)
const refreshing = ref(false)
const taskDueNowMs = useTaskDueClock() const taskDueNowMs = useTaskDueClock()
const mobileSidebar = ref(false) const mobileSidebar = ref(false)
const sidebarCollapsed = ref(false) const sidebarCollapsed = ref(false)
@@ -565,6 +566,17 @@ async function refreshAll() {
navigationLoaded.value = false navigationLoaded.value = false
await loadAll() await loadAll()
} }
async function refreshCurrentView() {
if (refreshing.value || loading.value) return
refreshing.value = true
try {
if (activeView.value === 'habits') await habitComposer.value?.refreshHabits()
else if (activeView.value === 'today') await Promise.all([refreshAll(), habitComposer.value?.refreshHabits()])
else await refreshAll()
} finally {
refreshing.value = false
}
}
type TrashPage = { items?: Task[]; total?: number } type TrashPage = { items?: Task[]; total?: number }
async function loadTrashPage() { async function loadTrashPage() {
return api(`/trash?page=${page.value}&page_size=${pageSize}`) as Promise<TrashPage> return api(`/trash?page=${page.value}&page_size=${pageSize}`) as Promise<TrashPage>
@@ -1312,7 +1324,10 @@ onUnmounted(() => {
<header class="topbar"> <header class="topbar">
<button class="icon" :aria-label="(sidebarCollapsed ? '展开' : '收起') + '菜单'" :aria-expanded="sidebarCollapsed ? 'false' : 'true'" @click="toggleSidebar"><Menu /></button> <button class="icon" :aria-label="(sidebarCollapsed ? '展开' : '收起') + '菜单'" :aria-expanded="sidebarCollapsed ? 'false' : 'true'" @click="toggleSidebar"><Menu /></button>
<div class="topbar-title"><h1 :title="activeName">{{ activeName }}</h1></div> <div class="topbar-title"><h1 :title="activeName">{{ activeName }}</h1></div>
<CompletedFilterPill v-if="['today', 'tasks', 'upcoming', 'habits'].includes(activeView)" v-model="showCompleted" class="topbar-filter" /> <div v-if="['today', 'tasks', 'upcoming', 'habits'].includes(activeView)" class="topbar-actions">
<CompletedFilterPill v-model="showCompleted" class="topbar-filter" />
<button class="icon topbar-refresh" :class="{spinning:refreshing}" type="button" aria-label="刷新当前页面" title="刷新" :disabled="refreshing || loading" @click="refreshCurrentView"><RefreshCw /></button>
</div>
<div v-if="taskSearchAvailable" class="search-reveal" :class="{'mobile-search-open':mobileSearchOpen || Boolean(query),'mobile-search-pulling':searchPullDistance>0}" :style="searchRevealStyle"> <div v-if="taskSearchAvailable" class="search-reveal" :class="{'mobile-search-open':mobileSearchOpen || Boolean(query),'mobile-search-pulling':searchPullDistance>0}" :style="searchRevealStyle">
<span class="search-pull-hint" aria-hidden="true">{{searchPullDistance >= 56 ? '松开搜索' : '下拉搜索'}}</span> <span class="search-pull-hint" aria-hidden="true">{{searchPullDistance >= 56 ? '松开搜索' : '下拉搜索'}}</span>
<label class="search"><Search/><input ref="searchInput" v-model="query" placeholder="搜索任务" aria-label="搜索任务" @blur="handleSearchBlur"><kbd>⌘ K</kbd></label> <label class="search"><Search/><input ref="searchInput" v-model="query" placeholder="搜索任务" aria-label="搜索任务" @blur="handleSearchBlur"><kbd>⌘ K</kbd></label>
+1 -1
View File
@@ -417,7 +417,7 @@ function closeHabitDetail() {
else habitArchiveToggle.value?.focus() else habitArchiveToggle.value?.focus()
}) })
} }
defineExpose({ openHabitComposer }) defineExpose({ openHabitComposer, refreshHabits: loadHabits })
async function archiveHabit(h: Habit) { async function archiveHabit(h: Habit) {
if (!confirm(`归档习惯“${h.name}”?历史打卡记录会保留。`)) return if (!confirm(`归档习惯“${h.name}”?历史打卡记录会保留。`)) return
await safe(async () => { await safe(async () => {
File diff suppressed because one or more lines are too long
+14 -2
View File
@@ -615,6 +615,15 @@ describe('task and habit row decoration', () => {
expect(app.match(/<CompletedFilterPill/g)).toHaveLength(1) expect(app.match(/<CompletedFilterPill/g)).toHaveLength(1)
expect(app).toContain("v-if=\"['today', 'tasks', 'upcoming', 'habits'].includes(activeView)\"") expect(app).toContain("v-if=\"['today', 'tasks', 'upcoming', 'habits'].includes(activeView)\"")
expect(app).toContain('v-model="showCompleted"') expect(app).toContain('v-model="showCompleted"')
expect(app).toContain('class="topbar-actions"')
expect(app).toContain('topbar-refresh')
expect(app).toContain('aria-label="刷新当前页面"')
expect(app).toContain('@click="refreshCurrentView"')
expect(app).toContain(':class="{spinning:refreshing}"')
expect(app).toContain(':disabled="refreshing || loading"')
expect(app).toContain("else if (activeView.value === 'today') await Promise.all([refreshAll(), habitComposer.value?.refreshHabits()])")
expect(app).toContain("if (activeView.value === 'habits') await habitComposer.value?.refreshHabits()")
expect(mvpPanel).toContain('defineExpose({ openHabitComposer, refreshHabits: loadHabits })')
expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)") expect(app).toContain("readStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, true)")
expect(app).toContain("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)") expect(app).toContain("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)")
expect(app).toContain(':show-completed="showCompleted"') expect(app).toContain(':show-completed="showCompleted"')
@@ -642,8 +651,11 @@ describe('task and habit row decoration', () => {
expect(css).toContain('.topbar .search{width:100%;margin:0}') expect(css).toContain('.topbar .search{width:100%;margin:0}')
expect(css).toContain('.topbar-title{grid-area:title;min-width:0;width:100%}') expect(css).toContain('.topbar-title{grid-area:title;min-width:0;width:100%}')
expect(css).toContain('.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;') expect(css).toContain('.topbar h1{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;')
expect(css).toContain('.topbar-filter{grid-area:filter;justify-self:end;') expect(css).toContain('.topbar-filter{justify-self:end;')
expect(app.indexOf('class="topbar-filter"')).toBeLessThan(app.indexOf('class="search-reveal"')) expect(css).toContain('.topbar-actions{grid-area:filter;justify-self:end;display:flex;align-items:center;gap:8px}')
expect(css).toContain('.topbar-refresh{width:44px;height:44px;')
expect(css).toContain('.topbar-refresh.spinning svg{animation:topbar-refresh-spin .7s linear infinite}')
expect(app.indexOf('class="topbar-actions"')).toBeLessThan(app.indexOf('class="search-reveal"'))
expect(app).toContain('@touchstart="startSearchPull"') expect(app).toContain('@touchstart="startSearchPull"')
expect(app).toContain('@touchmove="moveSearchPull"') expect(app).toContain('@touchmove="moveSearchPull"')
expect(app).toContain('@touchend="finishSearchPull"') expect(app).toContain('@touchend="finishSearchPull"')