refactor: 统一应用架构 — 消除前后台割裂,引入全局侧边栏、命令面板和 Motion 动效
- 移除独立的 /admin 路由层,路由扁平化为 /bookmarks - 用 AppSidebar 替代 AdminSidebar,SidebarProvider 提升至 _protected 全局布局 - 新增 ⌘K 命令面板(cmdk + @tanstack/react-hotkeys),支持书签搜索、搜索引擎跳转和页面导航 - 书签页查看/管理一体化,通过编辑模式开关切换,AnimatePresence 平滑过渡 - 总览驾驶舱:Motion stagger 入场动画、常用书签快捷区、模块概览卡片 - 统一设计语言:BookmarkCard/CategoryGrid 用 design token 替代硬编码 stone 色 - ModuleMetadata.adminRoute 重命名为 route - 同步更新 AGENTS.md 文档
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { Settings } from 'lucide-react'
|
||||
import * as icons from 'lucide-react'
|
||||
import { ArrowRight, Compass, Plus } from 'lucide-react'
|
||||
import * as motion from 'motion/react-client'
|
||||
import { orpc } from '@/client/orpc'
|
||||
import { CategoryGrid } from '@/modules/bookmarks/components/CategoryGrid'
|
||||
import { GreetingHeader } from '@/modules/bookmarks/components/GreetingHeader'
|
||||
import { SearchBar } from '@/modules/bookmarks/components/SearchBar'
|
||||
|
||||
const allIcons = icons as unknown as Record<string, React.ComponentType<{ className?: string }>>
|
||||
|
||||
export const Route = createFileRoute('/_protected/' as never)({
|
||||
loader: async ({ context }: { context: { queryClient: QueryClient } }) => {
|
||||
@@ -14,31 +15,124 @@ export const Route = createFileRoute('/_protected/' as never)({
|
||||
component: DashboardPage,
|
||||
})
|
||||
|
||||
const getGreeting = (hour: number): string => {
|
||||
if (hour >= 5 && hour < 12) return '早上好'
|
||||
if (hour >= 12 && hour < 14) return '中午好'
|
||||
if (hour >= 14 && hour < 18) return '下午好'
|
||||
return '晚上好'
|
||||
}
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const weekday = weekdays[date.getDay()]
|
||||
return `${year}年${month}月${day}日 ${weekday}`
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.06, delayChildren: 0.1 },
|
||||
},
|
||||
}
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 12 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] as const } },
|
||||
}
|
||||
|
||||
function DashboardPage() {
|
||||
const { data: categories } = useSuspenseQuery(orpc.bookmarks.category.list.queryOptions())
|
||||
const now = new Date()
|
||||
|
||||
const totalBookmarks = categories.reduce(
|
||||
(sum: number, cat: { bookmarks: Array<{ id: string }> }) => sum + cat.bookmarks.length,
|
||||
0,
|
||||
)
|
||||
const topBookmarks = categories
|
||||
.flatMap((cat: { bookmarks: Array<{ id: string; name: string; url: string; icon: string | null }> }) =>
|
||||
cat.bookmarks.slice(0, 4),
|
||||
)
|
||||
.slice(0, 8)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50/50 px-4 py-12 font-sans sm:px-6">
|
||||
<div className="mx-auto max-w-5xl space-y-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<GreetingHeader />
|
||||
<motion.div className="flex-1 px-6 pb-8" variants={containerVariants} initial="hidden" animate="visible">
|
||||
<motion.div variants={itemVariants} className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{getGreeting(now.getHours())}</h1>
|
||||
<p className="mt-1 text-muted-foreground">{formatDate(now)}</p>
|
||||
</motion.div>
|
||||
|
||||
{topBookmarks.length > 0 && (
|
||||
<motion.div variants={itemVariants} className="mb-8">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-medium text-muted-foreground">常用书签</h2>
|
||||
<Link
|
||||
to={'/bookmarks' as never}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
查看全部
|
||||
<ArrowRight className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{topBookmarks.map((bookmark: { id: string; name: string; url: string; icon: string | null }) => {
|
||||
const Icon = (bookmark.icon && allIcons[bookmark.icon]) || icons.Globe
|
||||
return (
|
||||
<motion.a
|
||||
key={bookmark.id}
|
||||
href={bookmark.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center gap-3 rounded-xl border bg-card px-3.5 py-3 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted/60 transition-colors group-hover:bg-muted">
|
||||
<Icon className="size-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
</div>
|
||||
<span className="min-w-0 truncate text-sm font-medium">{bookmark.name}</span>
|
||||
</motion.a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<h2 className="mb-4 text-sm font-medium text-muted-foreground">概览</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Link
|
||||
to={'/admin' as never}
|
||||
className="rounded-full p-2.5 text-stone-400 transition-colors hover:bg-stone-100 hover:text-stone-600"
|
||||
title="管理后台"
|
||||
to={'/bookmarks' as never}
|
||||
className="group flex items-center gap-4 rounded-xl border bg-card p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted/60 transition-colors group-hover:bg-muted">
|
||||
<Compass className="size-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">书签导航</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{categories.length} 个分类 · {totalBookmarks} 个书签
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="size-4 text-muted-foreground opacity-0 transition-all group-hover:opacity-100" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to={'/bookmarks' as never}
|
||||
className="group flex items-center gap-4 rounded-xl border border-dashed bg-card p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted/60 transition-colors group-hover:bg-muted">
|
||||
<Plus className="size-5 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">添加书签</p>
|
||||
<p className="text-xs text-muted-foreground">快速添加常用链接</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-2xl py-4">
|
||||
<SearchBar />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<CategoryGrid categories={categories} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user