/** * # 路由 /admin 后台布局 * 左侧 Sider 菜单 + 顶部 Header(当前用户 / 退出)。 * 未登录或无后台权限:整页渲染登录页(不再展示后台骨架)。 */ import { createFileRoute, Outlet, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { App, Avatar, Button, Card, ConfigProvider, Form, Input, Layout, Menu, Space, Tag } from "antd"; import { LockKeyhole } from "lucide-react"; import { DatabaseBackup, Files, Inbox, Image as ImageIcon, Languages, LayoutDashboard, List, LogOut, Package, Route as RouteIcon, ScrollText, Settings, ShoppingCart, SlidersHorizontal, UserRound, } from "lucide-react"; import { useState, useEffect, type ReactNode } from "react"; import { StyleProvider } from "@ant-design/cssinjs"; import { getCurrentUser, loginFn, logoutFn } from "~/server/auth"; import { getLoginSecurityPublic } from "~/server/security"; import { RecaptchaField, useRecaptcha, type LoginSecurityPublic } from "~/components/admin/Recaptcha"; /** # 类型 后台菜单分组(分组标签不可点;key 即目标路由路径) **/ const MENU_GROUPS: { label: string; children: { key: string; icon: ReactNode; label: string }[] }[] = [ { label: "概览", children: [{ key: "/admin/", icon: , label: "Dashboard" }], }, { label: "内容", children: [ { key: "/admin/contents/", icon: , label: "页面 / 文章" }, { key: "/admin/menus/", icon: , label: "菜单" }, { key: "/admin/media/", icon: , label: "媒体库" }, ], }, { label: "商务", children: [ { key: "/admin/products/", icon: , label: "产品" }, { key: "/admin/orders/", icon: , label: "订单" }, { key: "/admin/inquiries/", icon: , label: "询盘" }, ], }, { label: "系统", children: [ { key: "/admin/filters/", icon: , label: "筛选" }, { key: "/admin/languages/", icon: , label: "语言" }, { key: "/admin/seo/", icon: , label: "SEO / 重定向" }, { key: "/admin/users/", icon: , label: "用户" }, { key: "/admin/settings/", icon: , label: "系统设置" }, { key: "/admin/backups/", icon: , label: "备份" }, { key: "/admin/audit/", icon: , label: "审计日志" }, ], }, ]; /** # 类型 后台菜单项(扁平视图,用于路径高亮匹配) **/ const MENU_ITEMS = MENU_GROUPS.flatMap((g) => g.children); /** # 类型 合法菜单路径(用于类型安全的导航) **/ type MenuKey = string; /** # 类型 拥有后台权限的角色 **/ const ADMIN_ROLES = ["admin", "editor", "manager"]; export const Route = createFileRoute("/admin")({ loader: () => getCurrentUser(), component: AdminLayout, // AntD 烘焙静态样式(scripts/gen-antd-css.tsx 生成到 public/): // 用 而非 JS import —— dev 下 vite 会把 import 的 CSS 注入成