From 7957e5cee5faaba1502ca5b90945b08c5c2417a5 Mon Sep 17 00:00:00 2001 From: XingfenD Date: Tue, 8 Sep 2026 17:35:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20AppShell=20layout=20route=20?= =?UTF-8?q?=E2=80=94=20desktop=20rail,=20tablet=20icon=20rail,=20mobile=20?= =?UTF-8?q?tabs,=20theme=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 30 ++---- frontend/src/components/app-shell.tsx | 131 +++++++++++++++++++++++ frontend/src/components/theme-toggle.tsx | 32 ++++++ 3 files changed, 170 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/app-shell.tsx create mode 100644 frontend/src/components/theme-toggle.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dc4ff58..f319bcc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { Navigate, Route, Routes } from "react-router-dom"; import { AuthProvider, RequireAdmin, RequireAuth } from "./auth/AuthContext"; +import { AppShell } from "./components/app-shell"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { Toaster } from "./components/Toaster"; import { ThemeProvider } from "./components/theme"; @@ -17,13 +18,16 @@ export default function App() { } /> - + } - /> + > + } /> + } /> + } /> + } /> - - - - - - } - /> - - - - - - } - /> } /> diff --git a/frontend/src/components/app-shell.tsx b/frontend/src/components/app-shell.tsx new file mode 100644 index 0000000..32a78d7 --- /dev/null +++ b/frontend/src/components/app-shell.tsx @@ -0,0 +1,131 @@ +import { useQuery } from "@tanstack/react-query"; +import { BookMarked, BookOpen, FolderCog, LogOut, Users } from "lucide-react"; +import { NavLink, Outlet, useLocation } from "react-router-dom"; +import { api } from "@/api/client"; +import { useAuth } from "@/auth/AuthContext"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Separator } from "@/components/ui/separator"; +import { ThemeToggle } from "./theme-toggle"; +import { cn } from "@/lib/utils"; +import type { ReactNode } from "react"; + +function Brand({ compact }: { compact?: boolean }) { + return ( + + + + + {!compact && BookLib} + + ); +} + +function NavItem({ to, icon, label, search }: { to: string; icon: ReactNode; label: string; search?: string }) { + return ( + + cn( + "flex items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground", + isActive && "bg-secondary font-medium text-foreground", + ) + } + > + {icon} + {label} + + ); +} + +export function AppShell() { + const { user, isAdmin, logout } = useAuth(); + const loc = useLocation(); + const libsQ = useQuery({ queryKey: ["libraries"], queryFn: api.listLibraries }); + const libs = libsQ.data ?? []; + + const account = ( + + + + + + {user?.username}{isAdmin ? " · admin" : ""} + + + + 退出登录 + + + + + ); + + return ( +
+ {/* 桌面/平板侧栏;lg 全文标签,md 图标栏 */} + + +
+ {/* 移动端顶栏 */} +
+ +
+ + {account} +
+
+
+ +
+ {/* 移动端底 Tab */} + +
+
+ ); +} + +function TabItem({ to, icon, label, search }: { to: string; icon: ReactNode; label: string; search?: string }) { + return ( + cn("flex flex-col items-center justify-center gap-0.5 text-[11px] text-muted-foreground", isActive && "text-accent")}> + {icon} + {label} + + ); +} diff --git a/frontend/src/components/theme-toggle.tsx b/frontend/src/components/theme-toggle.tsx new file mode 100644 index 0000000..53a8dd7 --- /dev/null +++ b/frontend/src/components/theme-toggle.tsx @@ -0,0 +1,32 @@ +import { Check, Monitor, Moon, Sun } from "lucide-react"; +import { useTheme, type Theme } from "./theme"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; + +const ITEMS: { v: Theme; label: string; icon: typeof Sun }[] = [ + { v: "light", label: "浅色", icon: Sun }, + { v: "dark", label: "深色", icon: Moon }, + { v: "system", label: "跟随系统", icon: Monitor }, +]; + +export function ThemeToggle() { + const { theme, setTheme } = useTheme(); + return ( + + + + + + {ITEMS.map(({ v, label, icon: Icon }) => ( + setTheme(v)}> + + {label} + {theme === v && } + + ))} + + + ); +}