- Drop redundant <h2> page titles in 习惯 / 倒数日 / 设置与数据
- Wrap today view in three <section class='today-block'> sub-blocks:
今日任务 / 今日习惯 / 倒数日提醒 (7 天内 countdowns)
- Reduce task row noise: priority becomes a colored dot, priority
select and priority-text removed; delete action is desktop-only
- Hide task row inline actions and pager when only a single page
- Add a row-level More menu on sidebar list / folder rows (rename,
archive, delete) and a 'archived' toggle that appears only when
archived lists exist
- Add 'archived-heading' / 'page-subtitle' / 'today-block' /
'priority-dot' / 'task-delete{display:none}' / 'row-menu' styles
and a mobile 'min-height:44px' hit area for sidebar row actions
- Refresh unit tests in src/style.test.ts to cover the cleanup
This commit is contained in:
+21
-21
@@ -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,
|
||||
GripVertical, Inbox, ListChecks, ListTodo, Menu, MoreHorizontal, Pencil, Plus, Search,
|
||||
Settings, Trash2, X, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { filterTasks, fromDateTimeLocal, groupTaskTree, renderMarkdown, toDateTimeLocal } from './lib/task-utils'
|
||||
@@ -37,7 +37,6 @@ const loading = ref(false)
|
||||
const mobileSidebar = ref(false)
|
||||
const sidebarCollapsed = ref(false)
|
||||
const mobileDetail = ref(false)
|
||||
const mobileMore = ref(false)
|
||||
const moreSettingsOpen = ref(false)
|
||||
const markdownPreview = ref(false)
|
||||
const showCompleted = ref(true)
|
||||
@@ -51,6 +50,8 @@ const taskSwipeStart = ref<{ id: string; x: number; y: number } | null>(null)
|
||||
const taskPointerStart = ref<{ id: string; x: number; y: number } | null>(null)
|
||||
const taskSwipeOffsets = ref<Record<string, number>>({})
|
||||
const taskComposeOpen = ref(false)
|
||||
const sidebarActionMenu = ref('')
|
||||
const archivedListsOpen = ref(false)
|
||||
const composeTitle = ref('')
|
||||
const composeListId = ref('')
|
||||
const composeDueAt = ref('')
|
||||
@@ -295,7 +296,7 @@ async function switchView(view: View, listId?: string) {
|
||||
activeView.value = view
|
||||
if (listId) activeList.value = listId
|
||||
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; taskComposeOpen.value = false
|
||||
if (view === 'trash') await loadTrash()
|
||||
else if (view === 'today') await loadTodayView()
|
||||
else if (!isTaskView(view)) tasks.value = []
|
||||
@@ -459,6 +460,8 @@ async function restoreList(item: TaskList) {
|
||||
try { await api(`/lists/${item.id}/restore`, { method: 'POST' }); await loadArchivedLists(); await refreshAll(); toast('清单已恢复') } catch (reason) { fail(reason) }
|
||||
}
|
||||
function toggleFolder(id: string) { const next = new Set(expandedFolders.value); next.has(id) ? next.delete(id) : next.add(id); expandedFolders.value = next }
|
||||
function toggleSidebarActionMenu(id: string) { sidebarActionMenu.value = sidebarActionMenu.value === id ? '' : id }
|
||||
function closeSidebarActionMenu() { sidebarActionMenu.value = '' }
|
||||
function formatDue(value: string | null) { return value ? new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)) : '' }
|
||||
function previousPage() {
|
||||
if (page.value <= 1 || loading.value) return
|
||||
@@ -487,7 +490,7 @@ onMounted(bootstrap)
|
||||
</div>
|
||||
<div v-else class="shell" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
|
||||
<div v-if="mobileSidebar || mobileDetail" class="scrim" @click="mobileSidebar=false;mobileDetail=false" />
|
||||
<aside class="sidebar" :class="{ open: mobileSidebar }">
|
||||
<aside class="sidebar" :class="{ open: mobileSidebar }" @click="closeSidebarActionMenu">
|
||||
<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>
|
||||
@@ -500,13 +503,13 @@ onMounted(bootstrap)
|
||||
<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="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="重命名文件夹" @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><button class="mini-icon sidebar-more mobile-only" aria-label="文件夹操作" @click.stop="toggleSidebarActionMenu(`folder:${folder.id}`)"><MoreHorizontal/></button><div v-if="sidebarActionMenu===`folder:${folder.id}`" class="sidebar-row-menu" @click="closeSidebarActionMenu"><button @click="renameEntity('folders',folder)"><Pencil/>重命名</button><button @click="createList(folder.id)"><Plus/>新建清单</button><button @click="deleteEntity('folders',folder)"><Trash2/>删除</button></div></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><button class="mini-icon sidebar-more mobile-only" aria-label="清单操作" @click.stop="toggleSidebarActionMenu(`list:${list.id}`)"><MoreHorizontal/></button><div v-if="sidebarActionMenu===`list:${list.id}`" class="sidebar-row-menu" @click="closeSidebarActionMenu"><button @click="renameEntity('lists',list)"><Pencil/>重命名</button><button @click="deleteEntity('lists',list)"><ArchiveRestore/>归档</button></div></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="重命名清单" @click="renameEntity('lists',list)"><Pencil/></button><button aria-label="归档清单" @click="deleteEntity('lists',list)"><ArchiveRestore/></button></span><button class="mini-icon sidebar-more mobile-only" aria-label="清单操作" @click.stop="toggleSidebarActionMenu(`list:${list.id}`)"><MoreHorizontal/></button><div v-if="sidebarActionMenu===`list:${list.id}`" class="sidebar-row-menu" @click="closeSidebarActionMenu"><button @click="renameEntity('lists',list)"><Pencil/>重命名</button><button @click="deleteEntity('lists',list)"><ArchiveRestore/>归档</button></div></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>
|
||||
<button class="section-title archived-heading" :aria-expanded="archivedListsOpen" @click="archivedListsOpen=!archivedListsOpen"><span>已归档清单</span><ChevronDown v-if="archivedListsOpen"/><ChevronRight v-else/></button>
|
||||
<template v-if="archivedListsOpen"><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>
|
||||
</template>
|
||||
</div>
|
||||
<button class="settings" :class="{active:activeView==='settings'}" @click="switchView('settings')"><Settings />设置</button>
|
||||
@@ -523,29 +526,27 @@ onMounted(bootstrap)
|
||||
</template>
|
||||
<CountdownPanel ref="countdownComposer" v-else-if="activeView==='countdowns'" @notice="toast" />
|
||||
<template v-else>
|
||||
<template v-if="activeView==='today'">
|
||||
<h3 class="section-heading"><ListTodo/>今日任务</h3>
|
||||
</template>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span>第 {{page}} / {{totalPages}} 页 · 共 {{totalTasks}} 项</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<section :class="{ 'today-block': activeView==='today' }">
|
||||
<h3 v-if="activeView==='today'" class="section-heading"><ListTodo/>今日任务</h3>
|
||||
<div class="list-toolbar"><label v-if="activeView!=='trash'"><input v-model="showCompleted" type="checkbox"> 显示已完成</label><span>{{ totalPages > 1 ? `第 ${page} / ${totalPages} 页 · 共 ${totalTasks} 项` : `共 ${totalTasks} 项` }}</span><button v-if="query" class="link" @click="query=''">清除搜索</button></div>
|
||||
<div v-if="activeView!=='trash' && totalPages > 1" class="pager"><button class="secondary" :disabled="page<=1 || loading" @click="previousPage">上一页</button><span>{{page}} / {{totalPages}}</span><button class="secondary" :disabled="page>=totalPages || loading" @click="nextPage">下一页</button></div>
|
||||
<section class="task-list" :class="{loading}">
|
||||
<template v-for="node in taskTree" :key="node.task.id">
|
||||
<article class="task-row swipeable" :class="{done:node.task.completed,selected:selectedTask?.id===node.task.id,ready:Math.abs(taskSwipeOffsets[node.task.id] ?? 0) >= 64}" :style="{ '--swipe-x': `${taskSwipeOffsets[node.task.id] ?? 0}px` }" @pointerdown="startTaskPointer(node.task, $event)" @pointermove="moveTaskPointer(node.task, $event)" @pointerup="finishTaskPointer(node.task, $event)" @pointercancel="cancelTaskPointer(node.task)" @touchstart.passive="startTaskSwipe(node.task, $event)" @touchmove.passive="moveTaskSwipe(node.task, $event)" @touchend="finishTaskSwipe(node.task, $event)" @touchcancel="cancelTaskSwipe(node.task)">
|
||||
<div class="task-main" role="button" tabindex="0" @click="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task)" @keydown.enter="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task)" @keydown.space.prevent="activeView==='trash'?undefined:selectTaskUnlessSwiped(node.task)"><strong>{{node.task.title}}</strong><span class="meta"><span v-if="node.task.due_at"><CalendarDays/>{{formatDue(node.task.due_at)}}</span><span v-if="node.subtasks.length"><ListChecks/>{{node.subtasks.filter(t=>t.completed).length}}/{{node.subtasks.length}}</span></span></div>
|
||||
<button v-if="activeView!=='trash'" class="sr-only" :aria-label="node.task.completed ? `重新打开${node.task.title}` : `完成${node.task.title}`" :aria-pressed="node.task.completed" @click.stop="toggle(node.task)">切换任务状态</button>
|
||||
<span v-if="node.task.priority" class="priority" :class="`p${node.task.priority}`">{{['','低','中','高'][node.task.priority]}}</span>
|
||||
<span v-if="node.task.priority" class="priority-dot" :class="`p${node.task.priority}`" :title="`优先级:${['','低','中','高'][node.task.priority]}`"/>
|
||||
<button v-if="activeView==='trash'" class="restore" @click="restoreTask(node.task)"><ArchiveRestore/>恢复</button>
|
||||
<button v-else class="icon ghost" aria-label="删除任务" @click.stop="removeTask(node.task)"><Trash2/></button>
|
||||
<button v-else class="icon ghost task-delete" aria-label="删除任务" @click.stop="removeTask(node.task)"><Trash2/></button>
|
||||
<button v-if="activeView==='trash'" class="icon danger ghost" aria-label="永久删除" @click.stop="purgeTask(node.task)"><X/></button>
|
||||
</article>
|
||||
<article v-for="subtask in node.subtasks" :key="subtask.id" class="task-row subtask swipeable" :class="{done:subtask.completed,ready:Math.abs(taskSwipeOffsets[subtask.id] ?? 0) >= 64}" :style="{ '--swipe-x': `${taskSwipeOffsets[subtask.id] ?? 0}px` }" @pointerdown="startTaskPointer(subtask, $event)" @pointermove="moveTaskPointer(subtask, $event)" @pointerup="finishTaskPointer(subtask, $event)" @pointercancel="cancelTaskPointer(subtask)" @touchstart.passive="startTaskSwipe(subtask, $event)" @touchmove.passive="moveTaskSwipe(subtask, $event)" @touchend="finishTaskSwipe(subtask, $event)" @touchcancel="cancelTaskSwipe(subtask)"><GripVertical/><div class="task-main" role="button" tabindex="0" @click="selectTaskUnlessSwiped(subtask)" @keydown.enter="selectTaskUnlessSwiped(subtask)" @keydown.space.prevent="selectTaskUnlessSwiped(subtask)"><strong>{{subtask.title}}</strong></div><button class="sr-only" :aria-label="subtask.completed ? `重新打开${subtask.title}` : `完成${subtask.title}`" :aria-pressed="subtask.completed" @click.stop="toggle(subtask)">切换任务状态</button></article>
|
||||
</template>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty"><ListTodo/><b>{{query?'没有匹配的任务':'这里还很安静'}}</b><span>{{query?'换个关键词试试':'写下第一件想完成的小事吧'}}</span></div>
|
||||
<div v-if="!visibleTasks.length&&!loading" class="empty" :class="{ compact: activeView==='today' }"><ListTodo/><b>{{query?'没有匹配的任务':activeView==='today'?'今天没有任务':'这里还很安静'}}</b><span v-if="activeView!=='today'">{{query?'换个关键词试试':'写下第一件想完成的小事吧'}}</span></div>
|
||||
</section>
|
||||
<div v-if="activeView==='today'">
|
||||
<h3 class="section-heading"><Repeat2/>今日习惯</h3>
|
||||
<MvpPanel ref="habitComposer" view="today-habits" @changed="refreshAll" @notice="toast" />
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="activeView==='today'" class="today-block"><h3 class="section-heading"><Repeat2/>今日习惯</h3><MvpPanel ref="habitComposer" view="today-habits" @changed="refreshAll" @notice="toast" /></section>
|
||||
<section v-if="activeView==='today'" class="today-block"><h3 class="section-heading"><CalendarHeart/>倒数日提醒</h3><CountdownPanel view="today" @notice="toast" /></section>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
@@ -565,7 +566,6 @@ onMounted(bootstrap)
|
||||
<div v-else class="paper"><ListChecks/><b>选中一个任务</b><p>日期、优先级、子任务和 Markdown 备注会出现在这里。</p></div>
|
||||
</aside>
|
||||
|
||||
<div v-if="mobileMore" class="more-mask" @click.self="mobileMore=false;switchView('settings')"><section id="mobile-more-menu" class="more-sheet" role="dialog" aria-modal="true" aria-label="更多导航" @click.stop><div class="more-sheet-head"><b>更多</b><button class="icon" aria-label="关闭更多菜单" @click="mobileMore=false"><X/></button></div><button @click="switchView('settings')"><Settings/>设置与数据</button></section></div>
|
||||
<nav class="bottom" aria-label="主要导航"><button :class="{active:activeView==='today'}" @click="switchView('today')"><ListTodo/><span>今天</span></button><button :class="{active:activeView==='habits'}" @click="switchView('habits')"><Repeat2/><span>习惯</span></button><button :class="{active:activeView==='countdowns'}" @click="switchView('countdowns')"><CalendarHeart/><span>倒数日</span></button><button :class="{active:activeView==='tasks'||activeView==='upcoming'||activeView==='trash'||activeView==='settings'}" @click="switchView('settings')"><Settings/><span>设置</span></button></nav>
|
||||
<FloatingAddButton v-if="['tasks','today','upcoming','habits','countdowns'].includes(activeView)" :label="activeView==='habits' ? '添加习惯' : activeView==='countdowns' ? '添加倒数日' : '添加任务'" @activate="activateFloatingAdd" />
|
||||
<Transition name="task-compose">
|
||||
|
||||
@@ -12,6 +12,7 @@ type Countdown = {
|
||||
}
|
||||
type Form = { title:string; event_date:string; kind:Countdown['kind']; repeat_rule:Countdown['repeat_rule']; icon:string; calendar_mode:Countdown['calendar_mode']; lunar_year:number; lunar_month:number; lunar_day:number; leap_month:boolean; ignore_year:boolean }
|
||||
const emit = defineEmits<{ notice: [message: string] }>()
|
||||
const props = defineProps<{ view?: 'full' | 'today' }>()
|
||||
const items = ref<Countdown[]>([]), archived = ref<Countdown[]>([])
|
||||
const showArchived = ref(false), open = ref(false), busy = ref(false)
|
||||
const editingId = ref<string|null>(null), error = ref('')
|
||||
@@ -20,6 +21,7 @@ const freshForm = (): Form => ({ title:'', event_date:dateKey(new Date()), kind:
|
||||
const form = ref<Form>(freshForm())
|
||||
const composerOrigin = ref({ x: window.innerWidth - 43, y: window.innerHeight - 104 })
|
||||
const composerStyle = computed(() => ({ '--fab-origin-x': `${composerOrigin.value.x}px`, '--fab-origin-y': `${composerOrigin.value.y}px` }))
|
||||
const todayItems = computed(() => items.value.filter((item) => item.days >= 0 && item.days <= 7).slice(0, 3))
|
||||
const titleInput = ref<HTMLInputElement | null>(null)
|
||||
let previousFocus: HTMLElement | null = null
|
||||
|
||||
@@ -105,8 +107,12 @@ onBeforeUnmount(() => { previousFocus = null })
|
||||
|
||||
<template>
|
||||
<section class="countdown-view" :class="{ loading:busy }">
|
||||
<div class="countdown-content" :inert="open" :aria-hidden="open ? 'true' : undefined">
|
||||
<header class="countdown-hero"><div><small>记住值得期待与纪念的日子</small><h2>倒数日</h2></div></header>
|
||||
<div v-if="props.view==='today'" class="countdown-reminders">
|
||||
<article v-for="item in todayItems" :key="item.id"><span class="countdown-icon">{{item.icon}}</span><span><b>{{item.title}}</b><small>{{countdownDayText(item.days)}} · {{primaryDate(item)}}</small></span></article>
|
||||
<div v-if="!todayItems.length&&!busy" class="empty-panel compact">未来 7 天没有重要日子。</div>
|
||||
</div>
|
||||
<div v-else class="countdown-content" :inert="open" :aria-hidden="open ? 'true' : undefined">
|
||||
<header class="countdown-hero page-subtitle"><small>记住值得期待与纪念的日子</small></header>
|
||||
<p v-if="error" class="inline-error">{{error}}</p>
|
||||
<div class="countdown-grid">
|
||||
<article v-for="item in items" :key="item.id" class="countdown-card" :class="{ pinned:item.pinned }">
|
||||
@@ -120,7 +126,7 @@ onBeforeUnmount(() => { previousFocus = null })
|
||||
<button v-if="archived.length" class="archived-toggle" @click="showArchived=!showArchived"><ArchiveRestore/>已归档({{archived.length}})</button>
|
||||
<div v-if="showArchived" class="archived-countdowns"><article v-for="item in archived" :key="item.id"><span>{{item.icon}}</span><b>{{item.title}}</b><small>{{formatDateShort(item.display_date)}}<template v-if="item.lunar_text"> · {{item.lunar_text}}</template><template v-if="item.calendar_mode==='lunar'"> · 农历</template></small><button @click="restore(item)"><ArchiveRestore/>恢复</button><button class="danger-text" @click="purge(item)"><Trash2/>永久删除</button></article></div>
|
||||
</div>
|
||||
<Transition name="countdown-compose">
|
||||
<Transition v-if="props.view!=='today'" name="countdown-compose">
|
||||
<div v-if="open" class="countdown-modal-mask" @click.self="closeDialog"><form class="countdown-modal" :style="composerStyle" role="dialog" aria-modal="true" aria-labelledby="countdown-dialog-title" @submit.prevent="save" @keydown.esc="closeDialog" @keydown="trapDialogFocus">
|
||||
<header><div><small>{{editingId?'调整重要日子':'记下重要日子'}}</small><h3 id="countdown-dialog-title">{{editingId?'编辑倒数日':'新建倒数日'}}</h3></div><button type="button" aria-label="关闭" @click="closeDialog"><X/></button></header>
|
||||
<label>图标与名称<div class="countdown-title-fields"><input v-model="form.icon" maxlength="8" aria-label="图标"><input ref="titleInput" v-model="form.title" maxlength="200" required placeholder="例如:去北海道旅行" autofocus></div></label>
|
||||
|
||||
@@ -286,13 +286,13 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mvp-view" :class="{ loading: busy }">
|
||||
<section class="mvp-view" :class="{ loading: busy, 'today-habit-panel': view === 'today-habits' }">
|
||||
<p v-if="error" class="inline-error">{{ error }}</p>
|
||||
|
||||
<!-- 习惯(TickTick 风格:一次只操作一个习惯,不再逐格小按钮误触) -->
|
||||
<template v-if="view === 'habits' || view === 'today-habits'">
|
||||
<header v-if="view === 'habits'" class="view-intro">
|
||||
<div><small>把想坚持的事,变成每天的日常</small><h2>习惯</h2></div>
|
||||
<header v-if="view === 'habits'" class="view-intro page-subtitle">
|
||||
<small>把想坚持的事,变成每天的日常</small>
|
||||
<button class="soft-button" @click="loadHabits"><RefreshCw />刷新</button>
|
||||
</header>
|
||||
|
||||
@@ -336,8 +336,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 设置与数据 -->
|
||||
<template v-else>
|
||||
<header class="view-intro">
|
||||
<div><small>备份、迁移与安全</small><h2>设置与数据</h2></div>
|
||||
<header class="view-intro page-subtitle">
|
||||
<small>备份、迁移与安全</small>
|
||||
</header>
|
||||
<div class="settings-grid">
|
||||
<article class="tool-card"><FileJson /><h3>数据导出与恢复</h3><p>下载完整 JSON 备份,或从备份恢复。</p><button class="soft-button" @click="exportData"><Download />导出 JSON</button><label class="file-button"><ArchiveRestore />选择备份<input type="file" accept="application/json" @change="restoreFile=($event.target as HTMLInputElement).files?.[0]||null"></label><button v-if="restoreFile" class="danger-button" @click="restore">确认恢复</button></article>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -91,7 +91,7 @@ describe('unified floating add interaction', () => {
|
||||
expect(floatingAdd).toContain('@pointermove="moveDrag"')
|
||||
expect(floatingAdd).toContain("emit('activate',")
|
||||
expect(css).toContain('.unified-fab.dragging')
|
||||
expect(countdownPanel).toContain('<Transition name="countdown-compose">')
|
||||
expect(countdownPanel).toMatch(/<Transition[^>]*name="countdown-compose"/)
|
||||
expect(css).toContain('.countdown-compose-enter-active')
|
||||
expect(css).toContain('@media(max-width:930px){.unified-fab{bottom:calc(82px + env(safe-area-inset-bottom))}')
|
||||
})
|
||||
@@ -124,6 +124,48 @@ describe('unified floating add interaction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('first-pass information hierarchy cleanup', () => {
|
||||
it('removes repeated page titles while keeping lightweight subtitles', () => {
|
||||
expect(mvpPanel).not.toContain('<h2>习惯</h2>')
|
||||
expect(mvpPanel).not.toContain('<h2>设置与数据</h2>')
|
||||
expect(countdownPanel).not.toContain('<h2>倒数日</h2>')
|
||||
expect(mvpPanel).toContain('把想坚持的事,变成每天的日常')
|
||||
expect(countdownPanel).toContain('记住值得期待与纪念的日子')
|
||||
})
|
||||
|
||||
it('keeps single-page task counts without showing 1 / 1 pagination noise', () => {
|
||||
expect(app).toContain("totalPages > 1 ? `第 ${page} / ${totalPages} 页 · 共 ${totalTasks} 项` : `共 ${totalTasks} 项`")
|
||||
})
|
||||
|
||||
it('groups Today into task, habit, and countdown reminder blocks', () => {
|
||||
expect(app).toContain("<section :class=\"{ 'today-block': activeView==='today' }\">")
|
||||
expect(app.match(/class=\"today-block\"/g)?.length).toBe(2)
|
||||
expect(app).toContain('今日任务')
|
||||
expect(app).toContain('今日习惯')
|
||||
expect(app).toContain('倒数日提醒')
|
||||
expect(countdownPanel).toContain("const props = defineProps<{ view?: 'full' | 'today' }>()")
|
||||
expect(css).toContain('.today-block{')
|
||||
})
|
||||
|
||||
it('reduces task row noise to a priority dot and desktop-only delete action', () => {
|
||||
expect(app).toContain('class="priority-dot"')
|
||||
expect(app).not.toContain("{{['','低','中','高'][node.task.priority]}}")
|
||||
expect(app).toContain('class="icon ghost task-delete"')
|
||||
expect(css).toContain('.priority-dot')
|
||||
expect(css).toMatch(/\.task-delete\{display:none/)
|
||||
})
|
||||
|
||||
it('uses one mobile overflow trigger for sidebar row actions and folds archived lists by default', () => {
|
||||
expect(app).toContain("const sidebarActionMenu = ref('')")
|
||||
expect(app).toContain('class="mini-icon sidebar-more mobile-only"')
|
||||
expect(app).toContain('class="sidebar-row-menu"')
|
||||
expect(app).toContain('const archivedListsOpen = ref(false)')
|
||||
expect(app).toContain('v-if="archivedListsOpen"')
|
||||
expect(css).toContain('.sidebar-row-menu{')
|
||||
expect(css).toMatch(/\.row-actions\{display:none[^}]*\}/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('desktop sidebar collapse & folder scrollbar', () => {
|
||||
it('keeps overlay scrims out of the desktop grid layout', () => {
|
||||
expect(css).toContain('.scrim{display:none}')
|
||||
|
||||
Reference in New Issue
Block a user