diff --git a/frontend/components.json b/frontend/components.json index f1447d2..47149ed 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -5,5 +5,11 @@ "tsx": true, "tailwind": { "config": "", "css": "src/index.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", - "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" } + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } } diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 947ccc3..db55bda 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -39,11 +39,13 @@ export default tseslint.config( }, { files: ["e2e/**/*.ts"], + // e2e 是 Playwright 脚本,测试工具链的动态类型用法(如 locator/fixture 包装)允许 any languageOptions: { globals: { process: "readonly", console: "readonly", URL: "readonly" } }, rules: { "@typescript-eslint/no-explicit-any": "off" }, }, { files: ["**/*.test.{ts,tsx}", "test/**/*.ts"], + // 测试代码允许 any 以贴合测试惯例(mock 数据、运行时探测) rules: { "@typescript-eslint/no-explicit-any": "off" }, }, prettier, // 必须最后:关闭所有与 prettier 冲突的格式规则 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f319bcc..ed7c050 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,8 +25,22 @@ export default function App() { } > } /> - } /> - } /> + + + + } + /> + + + + } + /> (); const hasLS = typeof globalThis.localStorage?.setItem === "function"; export function getToken(): string | null { - return hasLS ? localStorage.getItem(TOKEN_KEY) : mem.get(TOKEN_KEY) ?? null; + return hasLS ? localStorage.getItem(TOKEN_KEY) : (mem.get(TOKEN_KEY) ?? null); } export function setToken(t: string | null): void { @@ -181,8 +181,7 @@ export const api = { listBookmarks: (id: number) => apiFetch(`/books/${id}/bookmarks`), createBookmark: (id: number, locator: Record, percent: number, note: string) => apiFetch(`/books/${id}/bookmarks`, { method: "POST", body: { locator, percent, note } }), - patchBookmark: (id: number, note: string) => - apiFetch(`/bookmarks/${id}`, { method: "PATCH", body: { note } }), + patchBookmark: (id: number, note: string) => apiFetch(`/bookmarks/${id}`, { method: "PATCH", body: { note } }), deleteBookmark: (id: number) => apiFetch(`/bookmarks/${id}`, { method: "DELETE" }), listProgress: () => apiFetch("/progress"), }; diff --git a/frontend/src/components/Bookmarks.tsx b/frontend/src/components/Bookmarks.tsx index be99188..3a87ecd 100644 --- a/frontend/src/components/Bookmarks.tsx +++ b/frontend/src/components/Bookmarks.tsx @@ -100,7 +100,11 @@ export function BookmarksTab({ > - diff --git a/frontend/src/components/Cover.tsx b/frontend/src/components/Cover.tsx index 8472df0..f606464 100644 --- a/frontend/src/components/Cover.tsx +++ b/frontend/src/components/Cover.tsx @@ -5,7 +5,11 @@ import { btn, formatBadge } from "./ui"; import { TriangleAlert } from "lucide-react"; export function useAuthedImage(url: string | undefined): { src: string; failed: boolean; retry: () => void } { - const [state, setState] = useState<{ key: string; src: string; failed: boolean }>({ key: "", src: "", failed: false }); + const [state, setState] = useState<{ key: string; src: string; failed: boolean }>({ + key: "", + src: "", + failed: false, + }); const [nonce, setNonce] = useState(0); const key = (url ?? "") + "#" + nonce; @@ -30,10 +34,10 @@ export function Cover({ book, className }: { book: Book; className?: string }) { const { src, failed, retry } = useAuthedImage(book.cover_url); if (!book.cover_url || failed) { return ( -
- {Array.from(book.title)[0] ?? "?"} +
+ + {Array.from(book.title)[0] ?? "?"} + {book.format} {failed && (
diff --git a/frontend/src/components/Toaster.tsx b/frontend/src/components/Toaster.tsx index 9678fad..1d26d86 100644 --- a/frontend/src/components/Toaster.tsx +++ b/frontend/src/components/Toaster.tsx @@ -36,7 +36,11 @@ export function Toaster() { t.kind === "ok" ? "border-border" : "border-destructive/40" }`} > - {t.kind === "ok" ? : } + {t.kind === "ok" ? ( + + ) : ( + + )} {t.text}
))} diff --git a/frontend/src/components/app-shell.tsx b/frontend/src/components/app-shell.tsx index 8e7b4f5..4e62aaa 100644 --- a/frontend/src/components/app-shell.tsx +++ b/frontend/src/components/app-shell.tsx @@ -4,7 +4,14 @@ import { Link, 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 { + 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"; @@ -12,7 +19,11 @@ import type { ReactNode } from "react"; function Brand() { return ( - + @@ -57,7 +68,10 @@ export function AppShell() { - {user?.username}{isAdmin ? " · admin" : ""} + + {user?.username} + {isAdmin ? " · admin" : ""} + {isAdmin && ( <> @@ -88,14 +102,25 @@ export function AppShell() {