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:
@@ -0,0 +1,161 @@
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Pencil, X } from 'lucide-react'
|
||||
import { AnimatePresence } from 'motion/react'
|
||||
import * as motion from 'motion/react-client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { orpc } from '@/client/orpc'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { BookmarkCard } from '@/modules/bookmarks/components/BookmarkCard'
|
||||
import { BookmarkManager } from '@/modules/bookmarks/components/BookmarkManager'
|
||||
import { CategoryManager } from '@/modules/bookmarks/components/CategoryManager'
|
||||
|
||||
export const Route = createFileRoute('/_protected/bookmarks' as never)({
|
||||
loader: async ({ context }: { context: { queryClient: QueryClient } }) => {
|
||||
await context.queryClient.fetchQuery(orpc.bookmarks.category.list.queryOptions())
|
||||
},
|
||||
component: BookmarksPage,
|
||||
})
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.05, delayChildren: 0.08 },
|
||||
},
|
||||
}
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] as const } },
|
||||
}
|
||||
|
||||
function BookmarksPage() {
|
||||
const { data: categories } = useSuspenseQuery(orpc.bookmarks.category.list.queryOptions())
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategoryId === null && categories.length > 0) {
|
||||
setSelectedCategoryId(categories[0]?.id ?? null)
|
||||
}
|
||||
}, [categories, selectedCategoryId])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCategoryId && !categories.some((c: { id: string }) => c.id === selectedCategoryId)) {
|
||||
setSelectedCategoryId(categories[0]?.id ?? null)
|
||||
}
|
||||
}, [categories, selectedCategoryId])
|
||||
|
||||
const selectedCategory = categories.find((c: { id: string }) => c.id === selectedCategoryId)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col px-6 pb-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{editing ? '书签管理' : '书签导航'}</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{editing ? '管理你的书签分类、图标和排序' : '常用链接和网站的快速导航'}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant={editing ? 'default' : 'outline'} size="sm" onClick={() => setEditing(!editing)}>
|
||||
{editing ? (
|
||||
<>
|
||||
<X className="size-4" />
|
||||
完成
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Pencil className="size-4" />
|
||||
编辑
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{editing ? (
|
||||
<motion.div
|
||||
key="edit"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.98 }}
|
||||
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] as const }}
|
||||
className="flex min-h-0 flex-1 gap-6"
|
||||
>
|
||||
<div className="w-80 shrink-0">
|
||||
<CategoryManager
|
||||
categories={categories}
|
||||
selectedCategoryId={selectedCategoryId}
|
||||
onSelectCategory={setSelectedCategoryId}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
{selectedCategory ? (
|
||||
<BookmarkManager category={selectedCategory} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded-xl border border-dashed text-sm text-muted-foreground">
|
||||
请选择一个分类开始管理书签
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="view"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit={{ opacity: 0 }}
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
{categories.length === 0 ? (
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col items-center justify-center py-32 text-center"
|
||||
>
|
||||
<div className="mb-4 flex size-16 items-center justify-center rounded-2xl bg-muted">
|
||||
<span className="text-2xl">✨</span>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-medium">还没有任何书签</h3>
|
||||
<p className="mb-6 text-sm text-muted-foreground">点击右上角「编辑」按钮添加你的第一个书签</p>
|
||||
<Button onClick={() => setEditing(true)}>
|
||||
<Pencil className="size-4" />
|
||||
开始添加
|
||||
</Button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-10 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map(
|
||||
(category: {
|
||||
id: string
|
||||
name: string
|
||||
bookmarks: Array<{ id: string; name: string; url: string; icon: string | null }>
|
||||
}) => (
|
||||
<motion.div key={category.id} variants={itemVariants} className="flex flex-col gap-3.5">
|
||||
<h2 className="px-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{category.name}
|
||||
</h2>
|
||||
{category.bookmarks.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed py-6 text-center">
|
||||
<span className="text-sm text-muted-foreground">暂无书签</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:grid-cols-1 xl:grid-cols-2">
|
||||
{category.bookmarks.map((bookmark) => (
|
||||
<BookmarkCard key={bookmark.id} bookmark={bookmark} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user