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 图标栏 */}
+
+
+
+ {/* 移动端顶栏 */}
+
+
+
+
+ {/* 移动端底 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 && }
+
+ ))}
+
+
+ );
+}