{"type":"assistant","message":{"id":"2099424a-0948-4423-a191-3c37d24e52ed","type":"message","role":"assistant","model":"deepseek-v4-flash","content":[{"type":"thinking","thinking":"The user wants me to generate a commit message based on the git diff provided. Let me analyze the changes:

1. **AdminLayout.tsx** - Major changes:
   - Added imports for `NavLink`, `useNavigate`, `BookOpenIcon`, `ListIcon`, `LogOutIcon`, `Button`, `useAuthStore`, `usePlatform`
   - Added navigation items array with book management and borrow management
   - Added admin layout with sidebar navigation, header with username, and logout functionality
   - Added responsive layout for desktop/mobile

2. **router/index.tsx** - Changes:
   - Removed placeholder components (`AdminBooksPage`, `AdminBorrowsPage`, `AdminIndexPage`)
   - Added imports from actual admin feature pages
   - Updated routes to use real admin layout and pages

This is clearly a **feat** (new feature) commit, adding admin layout with navigation and routing to real pages. The scope would be **admin** or **ui**.

The key changes are:
- Adding admin sidebar navigation with book/borrow management links
- Adding logout functionality with username display
- Replacing placeholder admin page components with real page implementations
- Integrating admin layout with the router

Let me write a concise commit message following the rules.","signature":"2099424a-0948-4423-a191-3c37d24e52ed"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25016,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard"},"context_management":null},"parent_tool_use_id":null,"session_id":"bf312622-fae8-446d-8b67-c708b9d3a022","uuid":"50eb9e22-cc58-4b45-8f03-95386c522466"}{"type":"assistant","message":{"id":"2099424a-0948-4423-a191-3c37d24e52ed","type":"message","role":"assistant","model":"deepseek-v4-flash","content":[{"type":"text","text":"<commit>
feat(admin): add sidebar navigation and real page routes

Implement admin layout with sidebar navigation for book and borrow
management, including logout functionality and username display.
Replace placeholder admin page components with actual page routes."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25016,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"service_tier":"standard"},"context_management":null},"parent_tool_use_id":null,"session_id":"bf312622-fae8-446d-8b67-c708b9d3a022","uuid":"1ca45e82-b885-4fa0-b886-fdfced0dc2e4"}<commit>
This commit is contained in:
2026-05-24 20:28:28 +08:00
parent 10dbfa7638
commit f28dc98f5e
11 changed files with 244 additions and 77 deletions
+23
View File
@@ -2,6 +2,22 @@ import axios, { AxiosError } from "axios"
import { useAuthStore } from "@/store/authStore"
import type { ApiResult } from "@/types/api"
function toCamelCase(s: string): string {
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
}
function transformKeys(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(transformKeys)
if (obj !== null && typeof obj === "object") {
const result: Record<string, unknown> = {}
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
result[toCamelCase(k)] = transformKeys(v)
}
return result
}
return obj
}
const client = axios.create({
baseURL: localStorage.getItem("api_base_url") || "",
timeout: 15000,
@@ -25,9 +41,13 @@ client.interceptors.response.use(
}
throw new Error(body.message || "Request failed")
}
response.data = transformKeys(body) as ApiResult<unknown>
return response
},
(error: AxiosError<ApiResult<unknown>>) => {
if (!navigator.onLine) {
throw new Error("网络连接失败,请检查网络")
}
const data = error.response?.data
if (data) {
if (data.code === 401) {
@@ -36,6 +56,9 @@ client.interceptors.response.use(
}
throw new Error(data.message || "Request failed")
}
if (error.code === "ERR_NETWORK" || error.code === "ECONNABORTED") {
throw new Error("网络连接失败,请检查网络")
}
throw error
}
)
+5 -3
View File
@@ -3,6 +3,7 @@ import { BookOpenIcon, ListIcon, LogOutIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useAuthStore } from "@/store/authStore"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
const navItems = [
{ to: "/admin/books", label: "图书管理", icon: BookOpenIcon },
@@ -14,7 +15,8 @@ export default function AdminLayout() {
const clearAuth = useAuthStore((s) => s.clearAuth)
const navigate = useNavigate()
const platform = usePlatform()
const isDesktop = platform === "web" || platform === "desktop"
const isMobile = useIsMobile()
const hideTitle = platform === "mobile"
function handleLogout() {
clearAuth()
@@ -26,7 +28,7 @@ export default function AdminLayout() {
isActive ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
}`
if (isDesktop) {
if (!isMobile) {
return (
<div className="flex min-h-screen">
<aside className="fixed top-0 left-0 flex h-full w-56 flex-col border-r bg-background">
@@ -43,7 +45,7 @@ export default function AdminLayout() {
</nav>
<div className="flex items-center gap-2 border-t px-4 py-3">
<span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">{username}</span>
<Button variant="ghost" size="icon-sm" onClick={handleLogout} title="退出">
<Button variant="ghost" size="icon-sm" onClick={handleLogout} {...(!hideTitle && { title: "退出" })}>
<LogOutIcon className="size-4" />
</Button>
</div>
+11 -9
View File
@@ -19,6 +19,7 @@ import {
} from "@/components/ui/alert-dialog"
import { useAdminBooks, useAddBook, useUpdateBook, useUpdateStock, useDeleteBook } from "./hooks"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
import type { Book } from "@/types/api"
// ---- Dialog state helpers ----
@@ -32,14 +33,15 @@ const addInitial: AddDialog = { open: false }
export default function AdminBooksPage() {
const { data: books, isLoading, error } = useAdminBooks()
const platform = usePlatform()
const isDesktop = platform === "web" || platform === "desktop"
const isMobile = useIsMobile()
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
const [add, setAdd] = useState<AddDialog>(addInitial)
const [edit, setEdit] = useState<EditDialog | null>(null)
const [stock, setStock] = useState<StockDialog | null>(null)
const [del, setDel] = useState<DeleteDialog>({ open: false, book: null })
if (isLoading) return <AdminBooksSkeleton isDesktop={isDesktop} />
if (isLoading) return <AdminBooksSkeleton isMobile={isMobile} />
if (error) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
@@ -62,7 +64,7 @@ export default function AdminBooksPage() {
{list.length === 0 ? (
<p className="py-16 text-center text-muted-foreground"></p>
) : isDesktop ? (
) : !isMobile ? (
<Table>
<TableHeader>
<TableRow>
@@ -77,8 +79,8 @@ export default function AdminBooksPage() {
{list.map((b) => (
<TableRow key={b.id}>
<TableCell>{b.id}</TableCell>
<TableCell className="font-medium">{b.name}</TableCell>
<TableCell>{b.author}</TableCell>
<TableCell className="font-medium" {...sel}>{b.name}</TableCell>
<TableCell {...sel}>{b.author}</TableCell>
<TableCell>{b.stock}</TableCell>
<TableCell>
<div className="flex gap-1">
@@ -106,8 +108,8 @@ export default function AdminBooksPage() {
<span className="text-xs text-muted-foreground">#{b.id}</span>
<span className="text-sm">{b.stock}</span>
</div>
<div className="font-medium">{b.name}</div>
<div className="text-sm text-muted-foreground">{b.author}</div>
<div className="font-medium" {...sel}>{b.name}</div>
<div className="text-sm text-muted-foreground" {...sel}>{b.author}</div>
<div className="flex gap-1.5 pt-2">
<Button size="xs" variant="outline" onClick={() => setEdit({ open: true, book: b })}>
@@ -301,8 +303,8 @@ function DeleteBookDialog({ book, open, onClose }: { book: Book; open: boolean;
}
// ---- Skeleton ----
function AdminBooksSkeleton({ isDesktop }: { isDesktop: boolean }) {
if (!isDesktop) {
function AdminBooksSkeleton({ isMobile }: { isMobile: boolean }) {
if (isMobile) {
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
+19 -13
View File
@@ -18,12 +18,14 @@ import {
} from "@/components/ui/alert-dialog"
import { useAdminBorrows, useSearchBorrows, useAdminReturnBook } from "./hooks"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
import { formatDateTime } from "@/lib/formatters"
import type { BorrowInfoVo } from "@/types/api"
export default function AdminBorrowsPage() {
const platform = usePlatform()
const isDesktop = platform === "web" || platform === "desktop"
const isMobile = useIsMobile()
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
const [searchText, setSearchText] = useState("")
const [debounced, setDebounced] = useState("")
@@ -41,7 +43,7 @@ export default function AdminBorrowsPage() {
const returnMutation = useAdminReturnBook()
const [returnTarget, setReturnTarget] = useState<BorrowInfoVo | null>(null)
if (isLoading) return <AdminBorrowsSkeleton isDesktop={isDesktop} />
if (isLoading) return <AdminBorrowsSkeleton isMobile={isMobile} />
if (error) {
return (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">
@@ -68,7 +70,7 @@ export default function AdminBorrowsPage() {
<p className="py-16 text-center text-muted-foreground">
{isSearching ? "无匹配记录" : "暂无借阅记录"}
</p>
) : isDesktop ? (
) : !isMobile ? (
<Table>
<TableHeader>
<TableRow>
@@ -86,9 +88,10 @@ export default function AdminBorrowsPage() {
<BorrowRow
key={r.id}
record={r}
isDesktop
isMobile={false}
returning={returnMutation.isPending}
onReturn={() => setReturnTarget(r)}
sel={sel}
/>
))}
</TableBody>
@@ -100,9 +103,10 @@ export default function AdminBorrowsPage() {
<CardContent className="space-y-1.5 p-4">
<BorrowRow
record={r}
isDesktop={false}
isMobile
returning={returnMutation.isPending}
onReturn={() => setReturnTarget(r)}
sel={sel}
/>
</CardContent>
</Card>
@@ -136,14 +140,16 @@ export default function AdminBorrowsPage() {
function BorrowRow({
record,
isDesktop,
isMobile,
returning,
onReturn,
sel,
}: {
record: BorrowInfoVo
isDesktop: boolean
isMobile: boolean
returning: boolean
onReturn: () => void
sel: Record<string, unknown>
}) {
const statusBadge = (
<Badge variant={record.status === "BORROWED" ? "default" : "secondary"}>
@@ -152,17 +158,17 @@ function BorrowRow({
)
const returnButton = record.status === "BORROWED" && (
<Button size={isDesktop ? "xs" : "sm"} variant="outline" disabled={returning} onClick={onReturn}>
<Button size={isMobile ? "sm" : "xs"} variant="outline" disabled={returning} onClick={onReturn}>
{returning && <Loader2Icon className="animate-spin" />}
</Button>
)
if (isDesktop) {
if (!isMobile) {
return (
<TableRow>
<TableCell>{record.id}</TableCell>
<TableCell className="font-medium">{record.bookBorrowVo.name}</TableCell>
<TableCell className="font-medium" {...sel}>{record.bookBorrowVo?.name ?? "-"}</TableCell>
<TableCell>{record.userBorrowVo.username}</TableCell>
<TableCell>{formatDateTime(record.borrowTime)}</TableCell>
<TableCell>{record.returnTime ? formatDateTime(record.returnTime) : "-"}</TableCell>
@@ -178,7 +184,7 @@ function BorrowRow({
<span className="text-xs text-muted-foreground">#{record.id}</span>
{statusBadge}
</div>
<div className="font-medium">{record.bookBorrowVo.name}</div>
<div className="font-medium" {...sel}>{record.bookBorrowVo?.name ?? "-"}</div>
<div className="text-sm text-muted-foreground">{record.userBorrowVo.username}</div>
<div className="text-sm text-muted-foreground">
{formatDateTime(record.borrowTime)}
@@ -189,8 +195,8 @@ function BorrowRow({
)
}
function AdminBorrowsSkeleton({ isDesktop }: { isDesktop: boolean }) {
if (!isDesktop) {
function AdminBorrowsSkeleton({ isMobile }: { isMobile: boolean }) {
if (isMobile) {
return (
<div className="space-y-3">
<Skeleton className="h-7 w-24" />
+4 -4
View File
@@ -6,7 +6,7 @@ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/componen
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useAuthStore } from "@/store/authStore"
import { useAuthStore, isAdminRole } from "@/store/authStore"
import { login } from "@/api/auth"
import { setBaseUrl } from "@/api/client"
@@ -27,7 +27,7 @@ export default function LoginPage() {
}, [])
if (token) {
return <Navigate to={role === "ADMIN" ? "/admin/books" : "/books"} replace />
return <Navigate to={isAdminRole(role) ? "/admin/books" : "/books"} replace />
}
function validate(): boolean {
@@ -49,7 +49,7 @@ export default function LoginPage() {
const result = await login({ username: username.trim(), password })
setAuth(result)
toast.success("登录成功")
navigate(result.role === "ADMIN" ? "/admin/books" : "/books", { replace: true })
navigate(isAdminRole(result.role) ? "/admin/books" : "/books", { replace: true })
} catch (err) {
toast.error(err instanceof Error ? err.message : "登录失败")
} finally {
@@ -59,7 +59,7 @@ export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center px-4">
<Card className="w-full max-w-md">
<Card className="w-full max-w-md mx-4 sm:mx-auto">
<CardHeader>
<CardTitle className="text-center text-xl"></CardTitle>
</CardHeader>
+11 -9
View File
@@ -6,15 +6,17 @@ import { Skeleton } from "@/components/ui/skeleton"
import { useBooks, useBorrowBook } from "./hooks"
import { useAuthStore } from "@/store/authStore"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
export default function BooksPage() {
const { data: books, isLoading, error } = useBooks()
const borrow = useBorrowBook()
const token = useAuthStore((s) => s.token)
const platform = usePlatform()
const isDesktop = platform === "web" || platform === "desktop"
const isMobile = useIsMobile()
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
if (isLoading) return <BooksSkeleton isDesktop={isDesktop} />
if (isLoading) return <BooksSkeleton isMobile={isMobile} />
if (error) {
return (
@@ -36,7 +38,7 @@ export default function BooksPage() {
)
}
if (isDesktop) {
if (!isMobile) {
return (
<div className="mx-auto max-w-[1200px] px-4 py-8">
<h1 className="mb-4 text-xl font-semibold"></h1>
@@ -52,8 +54,8 @@ export default function BooksPage() {
<TableBody>
{list.map((book) => (
<TableRow key={book.id}>
<TableCell className="font-medium">{book.name}</TableCell>
<TableCell>{book.author}</TableCell>
<TableCell className="font-medium" {...sel}>{book.name}</TableCell>
<TableCell {...sel}>{book.author}</TableCell>
<TableCell>{book.stock}</TableCell>
{token && (
<TableCell>
@@ -83,8 +85,8 @@ export default function BooksPage() {
<Card key={book.id}>
<CardContent className="flex items-center justify-between p-4">
<div className="space-y-0.5">
<div className="font-medium">{book.name}</div>
<div className="text-sm text-muted-foreground">{book.author}</div>
<div className="font-medium" {...sel}>{book.name}</div>
<div className="text-sm text-muted-foreground" {...sel}>{book.author}</div>
<div className="text-sm text-muted-foreground">{book.stock}</div>
</div>
{token && (
@@ -105,8 +107,8 @@ export default function BooksPage() {
)
}
function BooksSkeleton({ isDesktop }: { isDesktop: boolean }) {
if (!isDesktop) {
function BooksSkeleton({ isMobile }: { isMobile: boolean }) {
if (isMobile) {
return (
<div className="space-y-3 px-4 py-4">
<Skeleton className="h-7 w-24" />
+98 -33
View File
@@ -1,16 +1,22 @@
import { Loader2Icon } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { useMyBorrows, useReturnBook } from "./hooks"
import { formatDateTime } from "@/lib/formatters"
import { usePlatform } from "@/hooks/usePlatform"
import { useIsMobile } from "@/hooks/useIsMobile"
export default function MyBorrowsPage() {
const { data: records, isLoading, error } = useMyBorrows()
const returnBook = useReturnBook()
const platform = usePlatform()
const isMobile = useIsMobile()
const sel = platform === "web" ? ({ "data-selectable": true } as const) : ({} as const)
if (isLoading) return <MyBorrowsSkeleton />
if (isLoading) return <MyBorrowsSkeleton isMobile={isMobile} />
if (error) {
return (
@@ -33,36 +39,77 @@ export default function MyBorrowsPage() {
)
}
if (!isMobile) {
return (
<div className="mx-auto max-w-[1200px] px-4 py-8">
<h1 className="mb-4 text-xl font-semibold"></h1>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((r) => (
<TableRow key={r.id}>
<TableCell className="font-medium" {...sel}>{r.bookBorrowVo?.name ?? "-"}</TableCell>
<TableCell {...sel}>{r.bookBorrowVo?.author ?? "-"}</TableCell>
<TableCell>{formatDateTime(r.borrowTime)}</TableCell>
<TableCell>
{r.returnTime ? formatDateTime(r.returnTime) : "-"}
</TableCell>
<TableCell>
<Badge variant={r.status === "BORROWED" ? "default" : "secondary"}>
{r.status === "BORROWED" ? "借阅中" : "已归还"}
</Badge>
</TableCell>
<TableCell>
{r.status === "BORROWED" && (
<Button
size="sm"
variant="outline"
disabled={returnBook.isPending}
onClick={() => returnBook.mutate(r.id)}
>
{returnBook.isPending && <Loader2Icon className="animate-spin" />}
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
return (
<div className="mx-auto max-w-[1200px] px-4 py-8">
<div className="px-4 py-4">
<h1 className="mb-4 text-xl font-semibold"></h1>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{list.map((r) => (
<TableRow key={r.id}>
<TableCell className="font-medium">{r.bookBorrowVo.name}</TableCell>
<TableCell>{r.bookBorrowVo.author}</TableCell>
<TableCell>{formatDateTime(r.borrowTime)}</TableCell>
<TableCell>
{r.returnTime ? formatDateTime(r.returnTime) : "-"}
</TableCell>
<TableCell>
<div className="space-y-3">
{list.map((r) => (
<Card key={r.id}>
<CardContent className="space-y-1.5 p-4">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">#{r.id}</span>
<Badge variant={r.status === "BORROWED" ? "default" : "secondary"}>
{r.status === "BORROWED" ? "借阅中" : "已归还"}
</Badge>
</TableCell>
<TableCell>
{r.status === "BORROWED" && (
</div>
<div className="font-medium" {...sel}>{r.bookBorrowVo?.name ?? "-"}</div>
<div className="text-sm text-muted-foreground" {...sel}>{r.bookBorrowVo?.author ?? "-"}</div>
<div className="text-sm text-muted-foreground">
{formatDateTime(r.borrowTime)}
{r.returnTime && ` | 归还:${formatDateTime(r.returnTime)}`}
</div>
{r.status === "BORROWED" && (
<div className="pt-1">
<Button
size="sm"
variant="outline"
@@ -72,17 +119,35 @@ export default function MyBorrowsPage() {
{returnBook.isPending && <Loader2Icon className="animate-spin" />}
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
))}
</div>
</div>
)
}
function MyBorrowsSkeleton() {
function MyBorrowsSkeleton({ isMobile }: { isMobile: boolean }) {
if (isMobile) {
return (
<div className="space-y-3 px-4 py-4">
<Skeleton className="h-7 w-24" />
{Array.from({ length: 5 }).map((_, i) => (
<Card key={i}>
<CardContent className="space-y-2 p-4">
<Skeleton className="h-4 w-16" />
<Skeleton className="h-5 w-36" />
<Skeleton className="h-4 w-28" />
<Skeleton className="h-4 w-40" />
</CardContent>
</Card>
))}
</div>
)
}
return (
<div className="mx-auto max-w-[1200px] px-4 py-8">
<Skeleton className="mb-4 h-7 w-24" />
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useState } from "react"
const BP = "(max-width: 767px)"
export function useIsMobile(): boolean {
const [mobile, setMobile] = useState(() => window.matchMedia(BP).matches)
useEffect(() => {
const mql = window.matchMedia(BP)
function onChange(e: MediaQueryListEvent) {
setMobile(e.matches)
}
mql.addEventListener("change", onChange)
return () => mql.removeEventListener("change", onChange)
}, [])
return mobile
}
+44 -4
View File
@@ -1,5 +1,6 @@
export function formatDateTime(iso: string): string {
const d = new Date(iso)
export function formatDateTime(raw: unknown): string {
const d = toDate(raw)
if (!d) return "-"
const Y = d.getFullYear()
const M = String(d.getMonth() + 1).padStart(2, "0")
const D = String(d.getDate()).padStart(2, "0")
@@ -8,6 +9,45 @@ export function formatDateTime(iso: string): string {
return `${Y}-${M}-${D} ${h}:${m}`
}
export function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString()
export function formatDate(raw: unknown): string {
const d = toDate(raw)
if (!d) return "-"
return d.toLocaleDateString()
}
function toDate(raw: unknown): Date | null {
if (raw == null) return null
if (raw instanceof Date) return isNaN(raw.getTime()) ? null : raw
if (typeof raw === "string") {
const d = new Date(raw)
return isNaN(d.getTime()) ? null : d
}
if (typeof raw === "number") {
const d = new Date(raw)
return isNaN(d.getTime()) ? null : d
}
// Java LocalDateTime array: [year, month, day, hour, minute, second, nanos]
if (Array.isArray(raw) && raw.length >= 3) {
const [y, mo, d, h = 0, mi = 0, s = 0] = raw
return new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s))
}
if (typeof raw === "object") {
const obj = raw as Record<string, unknown>
const y = Number(obj.year ?? 0)
const mo = Number(obj.monthValue ?? obj.month ?? 1)
const d = Number(obj.dayOfMonth ?? obj.day ?? 1)
const h = Number(obj.hour ?? 0)
const mi = Number(obj.minute ?? 0)
const s = Number(obj.second ?? 0)
if (!isNaN(y) && !isNaN(mo) && !isNaN(d)) {
return new Date(y, mo - 1, d, h, mi, s)
}
}
return null
}
+1 -1
View File
@@ -9,7 +9,7 @@ export function ProtectedRoute() {
export function AdminRoute() {
const isLoggedIn = useAuthStore((s) => !!s.token)
const isAdmin = useAuthStore((s) => s.role === "ADMIN")
const isAdmin = useAuthStore((s) => s.isAdmin())
if (!isLoggedIn) return <Navigate to="/login" replace />
if (!isAdmin) return <Navigate to="/books" replace />
return <Outlet />
+10 -1
View File
@@ -45,10 +45,19 @@ export const useAuthStore = create<AuthState>((set, get) => {
},
isLoggedIn: () => !!get().token,
isAdmin: () => get().role === "ADMIN",
isAdmin: () => {
const r = get().role
return r != null && (r.toUpperCase() === "ADMIN" || r.toUpperCase() === "ROLE_ADMIN")
},
}
})
export function getToken(): string | null {
return useAuthStore.getState().token
}
export function isAdminRole(role: string | null): boolean {
if (!role) return false
const u = role.toUpperCase()
return u === "ADMIN" || u === "ROLE_ADMIN"
}