refactor: clarify sidebar navigation hierarchy
This commit is contained in:
+26
-11
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
ArchiveRestore, CalendarDays, CalendarHeart, Check, ChevronDown, ChevronRight, Folder,
|
||||
GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search,
|
||||
Ellipsis, GripVertical, Inbox, ListChecks, ListTodo, Menu, Pencil, Plus, Search,
|
||||
Settings, Trash2, X, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
|
||||
@@ -35,6 +35,8 @@ const restoredNavigation = readStoredNavigation(window.localStorage, NAVIGATION_
|
||||
const activeList = ref(restoredNavigation.listId)
|
||||
const activeView = ref<View>(restoredNavigation.view)
|
||||
const selectedTask = ref<Task | null>(null)
|
||||
const sidebarCreateOpen = ref(false)
|
||||
const sidebarAction = ref<{ kind: 'folders' | 'lists'; item: FolderItem | TaskList } | null>(null)
|
||||
const query = ref('')
|
||||
const error = ref('')
|
||||
const notice = ref('')
|
||||
@@ -468,7 +470,7 @@ async function switchView(view: View, listId?: string) {
|
||||
if (listId) activeList.value = listId
|
||||
writeStoredNavigation(window.localStorage, NAVIGATION_STORAGE_KEY, view, activeList.value)
|
||||
page.value = 1
|
||||
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false; mobileMore.value = false; taskComposeOpen.value = false
|
||||
selectedTask.value = null; mobileSidebar.value = false; mobileDetail.value = false; mobileMore.value = false; taskComposeOpen.value = false; sidebarCreateOpen.value = false; sidebarAction.value = null
|
||||
if (view === 'trash') await loadTrash()
|
||||
else if (view === 'today') await loadTodayView()
|
||||
else if (!isTaskView(view)) tasks.value = []
|
||||
@@ -741,6 +743,16 @@ async function loadArchivedLists() {
|
||||
async function restoreList(item: TaskList) {
|
||||
try { await api(`/lists/${item.id}/restore`, { method: 'POST' }); await loadArchivedLists(); await refreshAll(); toast('清单已恢复') } catch (reason) { fail(reason) }
|
||||
}
|
||||
function toggleSidebarCreate() { sidebarCreateOpen.value = !sidebarCreateOpen.value; sidebarAction.value = null }
|
||||
function openSidebarAction(kind: 'folders' | 'lists', item: FolderItem | TaskList) {
|
||||
sidebarAction.value = sidebarAction.value?.item.id === item.id ? null : { kind, item }
|
||||
sidebarCreateOpen.value = false
|
||||
}
|
||||
function runSidebarCreate(kind: 'folder' | 'list') {
|
||||
sidebarCreateOpen.value = false
|
||||
kind === 'folder' ? void createFolder() : void createList(null)
|
||||
}
|
||||
function closeSidebarAction() { sidebarAction.value = null }
|
||||
function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next }
|
||||
function formatDue(value: string | null, hasTime = true) {
|
||||
if (!value) return ''
|
||||
@@ -776,29 +788,32 @@ onMounted(bootstrap)
|
||||
</div>
|
||||
<div v-else class="shell" :class="{ 'sidebar-collapsed': sidebarCollapsed, 'detail-open': Boolean(selectedTask), 'mobile-sidebar-open': mobileSidebar }">
|
||||
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
|
||||
<aside class="sidebar" :class="{ open: mobileSidebar }">
|
||||
<aside class="sidebar" :class="{ open: mobileSidebar }" @keydown.esc="sidebarCreateOpen=false;closeSidebarAction()">
|
||||
<div class="brand-row"><div class="brand small">do<span>do</span></div><button class="icon mobile-only" aria-label="关闭菜单" @click="mobileSidebar=false"><X /></button></div>
|
||||
<nav class="primary-nav">
|
||||
<button :class="{ active: activeView==='tasks' && lists.find(l=>l.id===activeList)?.is_inbox }" @click="switchView('tasks', lists.find(l=>l.is_inbox)?.id)"><Inbox />收集箱</button>
|
||||
<button :class="{ active: activeView==='today' }" @click="switchView('today')"><ListTodo />今天</button>
|
||||
<button :class="{ active: activeView==='countdowns' }" @click="switchView('countdowns')"><CalendarHeart />倒数日</button>
|
||||
<button :class="{ active: activeView==='tasks' && lists.find(l=>l.id===activeList)?.is_inbox }" @click="switchView('tasks', lists.find(l=>l.is_inbox)?.id)"><Inbox />收集箱</button>
|
||||
<button :class="{ active: activeView==='upcoming' }" @click="switchView('upcoming')"><CalendarDays />最近 7 天</button>
|
||||
<button :class="{ active: activeView==='habits' }" @click="switchView('habits')"><Repeat2 />习惯</button>
|
||||
<button :class="{ active: activeView==='trash' }" @click="switchView('trash')"><Trash2 />回收站</button>
|
||||
<button :class="{ active: activeView==='countdowns' }" @click="switchView('countdowns')"><CalendarHeart />倒数日</button>
|
||||
</nav>
|
||||
<div class="section-title"><span>我的清单</span><span><button class="mini-icon" aria-label="新建文件夹" @click="createFolder"><Folder /></button><button class="mini-icon" aria-label="新建清单" @click="createList(null)"><Plus /></button></span></div>
|
||||
<div class="section-title"><span>我的清单</span><span class="sidebar-create-wrap"><button class="mini-icon list-create-trigger" aria-label="新建清单或文件夹" :aria-expanded="sidebarCreateOpen" @click="toggleSidebarCreate"><Plus /></button><span v-if="sidebarCreateOpen" class="sidebar-popover sidebar-create-menu"><button @click="runSidebarCreate('list')"><ListTodo/>新建清单</button><button @click="runSidebarCreate('folder')"><Folder/>新建文件夹</button></span></span></div>
|
||||
<div class="folders">
|
||||
<div v-for="folder in folders" :key="folder.id" class="folder-block">
|
||||
<div class="folder-row"><button :title="folder.name" :aria-label="folder.name" @click="toggleFolder(folder.id)"><ChevronDown v-if="expandedFolders.has(folder.id)"/><ChevronRight v-else/><Folder/><span>{{folder.name}}</span></button><span class="row-actions"><button aria-label="重命名文件夹" @click="renameEntity('folders',folder)"><Pencil/></button><button aria-label="删除文件夹" @click="deleteEntity('folders',folder)"><Trash2/></button><button aria-label="在文件夹中新建清单" @click="createList(folder.id)"><Plus/></button></span></div>
|
||||
<div v-for="list in lists.filter(l=>l.folder_id===folder.id && !l.is_inbox)" v-show="expandedFolders.has(folder.id)" :key="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id}"><button class="list-row-main" :title="list.name" :aria-label="list.name" @click="switchView('tasks',list.id)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="重命名清单" @click="renameEntity('lists',list)"><Pencil/></button><button aria-label="归档清单" @click="deleteEntity('lists',list)"><ArchiveRestore/></button></span></div>
|
||||
<div class="folder-row"><button :title="folder.name" :aria-label="folder.name" @click="toggleFolder(folder.id)"><ChevronDown v-if="expandedFolders.has(folder.id)"/><ChevronRight v-else/><Folder/><span>{{folder.name}}</span></button><span class="row-actions"><button aria-label="打开文件夹操作" :aria-expanded="sidebarAction?.item.id===folder.id" @click="openSidebarAction('folders',folder)"><Ellipsis/></button></span></div>
|
||||
<div v-for="list in lists.filter(l=>l.folder_id===folder.id && !l.is_inbox)" v-show="expandedFolders.has(folder.id)" :key="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id}"><button class="list-row-main" :title="list.name" :aria-label="list.name" @click="switchView('tasks',list.id)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
|
||||
</div>
|
||||
<div v-for="list in lists.filter(l=>!l.folder_id&&!l.is_inbox)" :key="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id}"><button class="list-row-main" :title="list.name" :aria-label="list.name" @click="switchView('tasks',list.id)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="重命名清单" @click="renameEntity('lists',list)"><Pencil/></button><button aria-label="归档清单" @click="deleteEntity('lists',list)"><ArchiveRestore/></button></span></div>
|
||||
<div v-for="list in lists.filter(l=>!l.folder_id&&!l.is_inbox)" :key="list.id" class="list-row" :class="{active:activeView==='tasks'&&activeList===list.id}"><button class="list-row-main" :title="list.name" :aria-label="list.name" @click="switchView('tasks',list.id)"><i/><span>{{list.name}}</span></button><span class="row-actions"><button aria-label="打开清单操作" :aria-expanded="sidebarAction?.item.id===list.id" @click="openSidebarAction('lists',list)"><Ellipsis/></button></span></div>
|
||||
<template v-if="archivedLists.length">
|
||||
<div class="section-title"><span>已归档清单</span></div>
|
||||
<div v-for="list in archivedLists" :key="list.id" class="list-row archived-row"><div class="list-row-main archived-row-label" :title="list.name" :aria-label="`已归档清单:${list.name}`"><ArchiveRestore/><span>{{list.name}}</span></div><span class="row-actions archived-actions"><button aria-label="恢复清单" @click="restoreList(list)"><ArchiveRestore/>恢复</button></span></div>
|
||||
</template>
|
||||
</div>
|
||||
<button class="settings" :class="{active:activeView==='settings'}" @click="switchView('settings')"><Settings />设置</button>
|
||||
<nav class="sidebar-management" aria-label="管理">
|
||||
<button :class="{active:activeView==='trash'}" @click="switchView('trash')"><Trash2 />回收站</button>
|
||||
<button :class="{active:activeView==='settings'}" @click="switchView('settings')"><Settings />设置</button>
|
||||
</nav>
|
||||
<div v-if="sidebarAction" class="sidebar-action-mask app-sheet-mask" @click.self="closeSidebarAction"><section class="sidebar-action-sheet app-sheet app-sheet--actions" role="dialog" aria-modal="true" :aria-label="`${sidebarAction.item.name}操作`"><header class="app-sheet__header"><b>{{sidebarAction.item.name}}</b><button class="icon" aria-label="关闭清单操作" @click="closeSidebarAction"><X/></button></header><div class="app-sheet__body"><button @click="renameEntity(sidebarAction.kind,sidebarAction.item);closeSidebarAction()"><Pencil/>重命名</button><button v-if="sidebarAction.kind==='folders'" @click="createList(sidebarAction.item.id);closeSidebarAction()"><Plus/>新建清单</button><button class="danger" @click="deleteEntity(sidebarAction.kind,sidebarAction.item);closeSidebarAction()"><component :is="sidebarAction.kind==='folders' ? Trash2 : ArchiveRestore"/>{{sidebarAction.kind==='folders'?'删除文件夹':'归档清单'}}</button></div></section></div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -478,6 +478,39 @@ describe('desktop sidebar collapse & folder scrollbar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('sidebar information hierarchy', () => {
|
||||
it('keeps daily navigation above lists and low-frequency management at the bottom', () => {
|
||||
const primary = app.slice(app.indexOf('<nav class="primary-nav">'), app.indexOf('</nav>', app.indexOf('<nav class="primary-nav">')))
|
||||
expect(primary).toContain('收集箱')
|
||||
expect(primary).toContain('今天')
|
||||
expect(primary).toContain('最近 7 天')
|
||||
expect(primary).toContain('习惯')
|
||||
expect(primary).toContain('倒数日')
|
||||
expect(primary).not.toContain('回收站')
|
||||
expect(app).toContain('class="sidebar-management"')
|
||||
expect(app).toMatch(/sidebar-management[\s\S]*?switchView\('trash'\)[\s\S]*?回收站[\s\S]*?switchView\('settings'\)[\s\S]*?设置/)
|
||||
})
|
||||
|
||||
it('uses a compact brand row, restrained active marker and a single list creation trigger', () => {
|
||||
expect(app).toContain('class="mini-icon list-create-trigger" aria-label="新建清单或文件夹"')
|
||||
expect(app).not.toContain('aria-label="新建文件夹" @click="createFolder"')
|
||||
expect(css).toContain('.brand-row{height:64px;')
|
||||
expect(css).toContain('.primary-nav button.active::before,.list-row.active::before,.sidebar-management button.active::before{')
|
||||
expect(css).toContain('width:3px;')
|
||||
})
|
||||
|
||||
it('collapses list management actions behind one explicit more button', () => {
|
||||
expect(app).toContain('aria-label="打开文件夹操作"')
|
||||
expect(app).toContain('aria-label="打开清单操作"')
|
||||
expect(app).toContain('class="sidebar-action-mask app-sheet-mask"')
|
||||
expect(app).toContain('class="sidebar-action-sheet app-sheet app-sheet--actions"')
|
||||
expect(app).toContain('@keydown.esc="sidebarCreateOpen=false;closeSidebarAction()"')
|
||||
expect(app).toContain('sidebarCreateOpen.value = false; sidebarAction.value = null')
|
||||
expect(app).not.toContain('aria-label="重命名文件夹" @click="renameEntity')
|
||||
expect(app).not.toContain('aria-label="重命名清单" @click="renameEntity')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sidebar layout', () => {
|
||||
it('switches to the compact layout before the desktop grid can be clipped', () => {
|
||||
expect(css).toContain('@media(max-width:930px){.shell')
|
||||
|
||||
Reference in New Issue
Block a user