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 {
|
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>
|
||||||
|
|||||||
@@ -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
@@ -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"')
|
||||||
|
|||||||
Reference in New Issue
Block a user