diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..41f3ad3 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,27 @@ +import { MoonIcon, SunIcon, MonitorIcon } from "lucide-react" +import { Button } from "@/components/ui/button" +import { useThemeStore, type ThemeMode } from "@/store/themeStore" +import { usePlatform } from "@/hooks/usePlatform" + +const next: Record = { system: "dark", dark: "light", light: "system" } +const icon: Record = { system: MonitorIcon, dark: MoonIcon, light: SunIcon } + +export default function ThemeToggle() { + const mode = useThemeStore((s) => s.mode) + const setMode = useThemeStore((s) => s.setMode) + const platform = usePlatform() + const hideTitle = platform === "mobile" + + const Icon = icon[mode] + + return ( + + ) +} diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx index e7bd075..cf7cefc 100644 --- a/src/components/layout/AdminLayout.tsx +++ b/src/components/layout/AdminLayout.tsx @@ -1,6 +1,7 @@ import { NavLink, Outlet, useNavigate } from "react-router-dom" import { BookOpenIcon, ListIcon, LogOutIcon } from "lucide-react" import { Button } from "@/components/ui/button" +import ThemeToggle from "@/components/ThemeToggle" import { useAuthStore } from "@/store/authStore" import { usePlatform } from "@/hooks/usePlatform" import { useIsMobile } from "@/hooks/useIsMobile" @@ -45,6 +46,7 @@ export default function AdminLayout() {
{username} + @@ -63,6 +65,7 @@ export default function AdminLayout() { 管理后台
{username} + diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index 5104211..9bf9c91 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -1,6 +1,7 @@ import { Link, Outlet, useNavigate } from "react-router-dom" import { useAuthStore } from "@/store/authStore" import { Button } from "@/components/ui/button" +import ThemeToggle from "@/components/ThemeToggle" export default function AppLayout() { const token = useAuthStore((s) => s.token) @@ -31,6 +32,7 @@ export default function AppLayout() { )}
+ {token ? ( <> {username} diff --git a/src/components/layout/UserLayout.tsx b/src/components/layout/UserLayout.tsx new file mode 100644 index 0000000..08bcf92 --- /dev/null +++ b/src/components/layout/UserLayout.tsx @@ -0,0 +1,109 @@ +import { NavLink, Outlet, useNavigate } from "react-router-dom" +import { BookOpenIcon, BookMarkedIcon, LogOutIcon } from "lucide-react" +import { Button } from "@/components/ui/button" +import ThemeToggle from "@/components/ThemeToggle" +import { useAuthStore } from "@/store/authStore" +import { usePlatform } from "@/hooks/usePlatform" +import { useIsMobile } from "@/hooks/useIsMobile" + +const navItems = [ + { to: "/books", label: "书目", icon: BookOpenIcon }, + { to: "/my-borrows", label: "我的借阅", icon: BookMarkedIcon }, +] + +export default function UserLayout() { + const username = useAuthStore((s) => s.username) + const token = useAuthStore((s) => s.token) + const clearAuth = useAuthStore((s) => s.clearAuth) + const navigate = useNavigate() + const platform = usePlatform() + const isMobile = useIsMobile() + const hideTitle = platform === "mobile" + + function handleLogout() { + clearAuth() + navigate("/books", { replace: true }) + } + + const linkClass = ({ isActive }: { isActive: boolean }) => + `flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${ + isActive ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground" + }` + + if (!isMobile) { + return ( +
+ +
+
+ +
+ +
+
+ ) + } + + return ( +
+
+ 图书管理系统 +
+ + {token ? ( + <> + {username} + + + ) : ( + + )} +
+
+
+ +
+ +
+ ) +} diff --git a/src/main.tsx b/src/main.tsx index 33ea2f8..a442fe7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { Toaster } from "@/components/ui/sonner" import { PlatformProvider } from "@/hooks/usePlatform" import { MobileClass } from "@/hooks/useIsMobile" +import { ThemeProvider } from "@/store/themeStore" import { router } from "@/router" import "./index.css" @@ -13,6 +14,7 @@ const queryClient = new QueryClient() ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + diff --git a/src/router/index.tsx b/src/router/index.tsx index b824c17..ff76271 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -7,6 +7,7 @@ import MyBorrowsPage from "@/features/borrows/MyBorrowsPage" import AdminBooksPage from "@/features/admin/AdminBooksPage" import AdminBorrowsPage from "@/features/admin/AdminBorrowsPage" import AppLayout from "@/components/layout/AppLayout" +import UserLayout from "@/components/layout/UserLayout" import AdminLayout from "@/components/layout/AdminLayout" function RootRedirect() { @@ -26,6 +27,11 @@ export const router = createBrowserRouter([ path: "/", element: , }, + ], + }, + { + element: , + children: [ { path: "/books", element: , diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts new file mode 100644 index 0000000..fbe6317 --- /dev/null +++ b/src/store/themeStore.ts @@ -0,0 +1,64 @@ +import { create } from "zustand" +import { useEffect } from "react" + +export type ThemeMode = "system" | "light" | "dark" + +interface ThemeState { + mode: ThemeMode + setMode: (mode: ThemeMode) => void +} + +function loadTheme(): ThemeMode { + try { + const v = localStorage.getItem("theme") + if (v === "light" || v === "dark" || v === "system") return v + } catch { /* unavailable */ } + return "system" +} + +function systemIsDark(): boolean { + return window.matchMedia("(prefers-color-scheme: dark)").matches +} + +export function effectiveTheme(mode: ThemeMode): "light" | "dark" { + if (mode === "system") return systemIsDark() ? "dark" : "light" + return mode +} + +function applyTheme(mode: ThemeMode) { + const root = document.documentElement + if (effectiveTheme(mode) === "dark") { + root.classList.add("dark") + } else { + root.classList.remove("dark") + } +} + +export const useThemeStore = create((set) => ({ + mode: loadTheme(), + setMode: (mode) => { + localStorage.setItem("theme", mode) + applyTheme(mode) + set({ mode }) + }, +})) + +export function ThemeProvider() { + const mode = useThemeStore((s) => s.mode) + + useEffect(() => { + applyTheme(mode) + }, [mode]) + + useEffect(() => { + if (mode !== "system") return + const mql = window.matchMedia("(prefers-color-scheme: dark)") + function sync() { + applyTheme("system") + } + mql.addEventListener("change", sync) + return () => mql.removeEventListener("change", sync) + }, [mode]) + + return null +}