feat: add current view refresh action
This commit is contained in:
+17
-2
@@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
ArchiveRestore, CalendarDays, CalendarHeart, Check, ChevronDown, ChevronRight, Folder,
|
||||
Ellipsis, GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search,
|
||||
Settings, Trash2, X, Repeat2,
|
||||
Settings, Trash2, X, Repeat2, RefreshCw,
|
||||
} 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 { 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 notice = ref('')
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const taskDueNowMs = useTaskDueClock()
|
||||
const mobileSidebar = ref(false)
|
||||
const sidebarCollapsed = ref(false)
|
||||
@@ -565,6 +566,17 @@ async function refreshAll() {
|
||||
navigationLoaded.value = false
|
||||
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 }
|
||||
async function loadTrashPage() {
|
||||
return api(`/trash?page=${page.value}&page_size=${pageSize}`) as Promise<TrashPage>
|
||||
@@ -1312,7 +1324,10 @@ onUnmounted(() => {
|
||||
<header class="topbar">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -417,7 +417,7 @@ function closeHabitDetail() {
|
||||
else habitArchiveToggle.value?.focus()
|
||||
})
|
||||
}
|
||||
defineExpose({ openHabitComposer })
|
||||
defineExpose({ openHabitComposer, refreshHabits: loadHabits })
|
||||
async function archiveHabit(h: Habit) {
|
||||
if (!confirm(`归档习惯“${h.name}”?历史打卡记录会保留。`)) return
|
||||
await safe(async () => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -615,6 +615,15 @@ describe('task and habit row decoration', () => {
|
||||
expect(app.match(/<CompletedFilterPill/g)).toHaveLength(1)
|
||||
expect(app).toContain("v-if=\"['today', 'tasks', 'upcoming', 'habits'].includes(activeView)\"")
|
||||
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("writeStoredBoolean(window.localStorage, SHOW_COMPLETED_STORAGE_KEY, value)")
|
||||
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-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-filter{grid-area:filter;justify-self:end;')
|
||||
expect(app.indexOf('class="topbar-filter"')).toBeLessThan(app.indexOf('class="search-reveal"'))
|
||||
expect(css).toContain('.topbar-filter{justify-self:end;')
|
||||
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('@touchmove="moveSearchPull"')
|
||||
expect(app).toContain('@touchend="finishSearchPull"')
|
||||
|
||||
Reference in New Issue
Block a user