feat(ui): [UI7-1] 重构主布局AppShell与导航层

这个提交包含在:
2026-03-06 09:48:11 +08:00
父节点 94d9bbe0b1
当前提交 9c48c8d3bc
共修改 5 个文件,包含 57 行新增和 37 行删除
+20 -6
查看文件
@@ -6,72 +6,85 @@ import { cn } from "../../lib/utils";
export type NavItem = { export type NavItem = {
to: string; to: string;
label: string; label: string;
icon: string;
anyOf?: string[]; anyOf?: string[];
}; };
export const NAV_ITEMS: NavItem[] = [ export const NAV_ITEMS: NavItem[] = [
{ to: "/dashboard", label: "仪表盘" }, { to: "/dashboard", label: "仪表盘", icon: "dashboard" },
{ {
to: "/reimbursements", to: "/reimbursements",
label: "我的报销", label: "我的报销",
icon: "request_quote",
anyOf: ["expense.view_own", "expense.view_all"], anyOf: ["expense.view_own", "expense.view_all"],
}, },
{ {
to: "/workflow/tasks", to: "/workflow/tasks",
label: "待我处理", label: "待我处理",
icon: "fact_check",
anyOf: ["expense.approve"], anyOf: ["expense.approve"],
}, },
{ {
to: "/invoice-pool", to: "/invoice-pool",
label: "票据中心", label: "票据中心",
icon: "receipt_long",
anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"], anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"],
}, },
{ {
to: "/mobile/capture", to: "/mobile/capture",
label: "移动拍照", label: "移动拍照",
icon: "document_scanner",
anyOf: ["invoice.upload", "invoice.manage"], anyOf: ["invoice.upload", "invoice.manage"],
}, },
{ {
to: "/mobile/approvals", to: "/mobile/approvals",
label: "移动审批", label: "移动审批",
icon: "mobile_friendly",
anyOf: ["expense.approve"], anyOf: ["expense.approve"],
}, },
{ {
to: "/budgets", to: "/budgets",
label: "预算管理", label: "预算管理",
icon: "account_balance",
anyOf: ["budget.view", "budget.manage"], anyOf: ["budget.view", "budget.manage"],
}, },
{ {
to: "/audit-logs", to: "/audit-logs",
label: "审计工作台", label: "审计工作台",
icon: "manage_search",
anyOf: ["system.audit.view"], anyOf: ["system.audit.view"],
}, },
{ {
to: "/delegations", to: "/delegations",
label: "委派管理", label: "委派管理",
icon: "swap_horiz",
anyOf: ["expense.proxy_manage"], anyOf: ["expense.proxy_manage"],
}, },
{ {
to: "/system/users", to: "/system/users",
label: "用户管理", label: "用户管理",
icon: "group",
anyOf: ["system.users"], anyOf: ["system.users"],
}, },
{ {
to: "/system/departments", to: "/system/departments",
label: "部门管理", label: "部门管理",
icon: "account_tree",
anyOf: ["system.users"], anyOf: ["system.users"],
}, },
{ {
to: "/system/approval-rules", to: "/system/approval-rules",
label: "审批规则", label: "审批规则",
icon: "rule",
anyOf: ["system.settings"], anyOf: ["system.settings"],
}, },
{ {
to: "/system/feature-flags", to: "/system/feature-flags",
label: "功能开关", label: "功能开关",
icon: "toggle_on",
anyOf: ["system.settings"], anyOf: ["system.settings"],
}, },
{ to: "/settings/profile", label: "个人设置" }, { to: "/settings/profile", label: "个人设置", icon: "manage_accounts" },
]; ];
export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
@@ -94,14 +107,15 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
onClick={() => onNavigate?.()} onClick={() => onNavigate?.()}
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
"block rounded-lg px-3 py-2.5 text-sm font-medium transition-colors", "flex items-center gap-3 px-3 py-2 my-1 rounded-lg text-sm transition-colors",
isActive isActive
? "bg-primary-50 text-primary" ? "bg-zinc-100 font-bold text-zinc-900"
: "text-slate-700 hover:bg-slate-50 hover:text-slate-900", : "font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900",
) )
} }
> >
{item.label} <span className="material-symbols-outlined text-[20px]">{item.icon}</span>
<span>{item.label}</span>
</NavLink> </NavLink>
))} ))}
</nav> </nav>
+5 -5
查看文件
@@ -2,13 +2,13 @@ import { NavLinks } from "./NavLinks";
export function Sidebar() { export function Sidebar() {
return ( return (
<aside className="hidden w-64 shrink-0 border-r border-slate-200 bg-white lg:flex lg:flex-col"> <aside className="hidden w-64 border-r border-zinc-200 bg-white lg:flex lg:flex-col fixed inset-y-0 z-20">
<div className="border-b border-slate-200 px-6 py-5"> <div className="border-b border-zinc-200 p-6 flex items-center gap-3">
<p className="text-sm font-semibold uppercase tracking-wide text-primary">hyb-oa</p> <div className="size-8 bg-zinc-900 rounded-lg flex items-center justify-center text-white font-bold text-lg">H</div>
<h1 className="mt-1 text-lg font-bold text-slate-900">海音达工作台</h1> <span className="font-bold text-zinc-900 tracking-tight">海音达工作台</span>
</div> </div>
<div className="flex-1 p-4"> <div className="flex-1 p-3">
<NavLinks /> <NavLinks />
</div> </div>
</aside> </aside>
+9 -9
查看文件
@@ -76,7 +76,7 @@ export function Topbar() {
const unreadBadge = unreadCount > 99 ? "99+" : String(unreadCount); const unreadBadge = unreadCount > 99 ? "99+" : String(unreadCount);
return ( return (
<header className="sticky top-0 z-20 border-b border-slate-200 bg-white/95 px-4 py-3 backdrop-blur sm:px-6"> <header className="sticky top-0 z-10 h-16 flex flex-col justify-center border-b border-zinc-200 bg-white px-4 sm:px-8">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Dialog open={mobileNavOpen} onOpenChange={setMobileNavOpen}> <Dialog open={mobileNavOpen} onOpenChange={setMobileNavOpen}>
@@ -94,12 +94,12 @@ export function Topbar() {
<DialogContent className="left-0 top-0 h-full w-72 max-w-[80vw] translate-x-0 translate-y-0 rounded-none rounded-r-xl p-0"> <DialogContent className="left-0 top-0 h-full w-72 max-w-[80vw] translate-x-0 translate-y-0 rounded-none rounded-r-xl p-0">
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-4"> <div className="flex items-center justify-between border-b border-zinc-200 px-4 py-4">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-wide text-primary"> <p className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
hyb-oa <span className="text-secondary capitalize">{me?.roleCodes?.[0] || 'User'}</span>
</p> </p>
<p className="mt-1 text-sm font-bold text-slate-900">海音达工作台</p> <p className="mt-1 text-sm font-bold text-zinc-900">海音达工作台</p>
</div> </div>
<DialogClose asChild> <DialogClose asChild>
<Button size="icon" variant="ghost" aria-label="关闭菜单" type="button"> <Button size="icon" variant="ghost" aria-label="关闭菜单" type="button">
@@ -116,8 +116,8 @@ export function Topbar() {
</Dialog> </Dialog>
<div> <div>
<p className="text-sm text-secondary">欢迎回来</p> <p className="text-sm text-zinc-500">欢迎回来</p>
<p className="text-base font-semibold text-slate-900">{me?.name ?? "-"}</p> <p className="text-base font-semibold text-zinc-900">{me?.name ?? "-"}</p>
</div> </div>
</div> </div>
@@ -141,8 +141,8 @@ export function Topbar() {
</Button> </Button>
<div className="hidden text-right sm:block"> <div className="hidden text-right sm:block">
<p className="text-xs uppercase tracking-wide text-secondary">角色</p> <p className="text-xs uppercase tracking-wide text-zinc-500">角色</p>
<p className="text-sm font-medium text-slate-900">{roleCodes.join(" / ") || "-"}</p> <p className="text-sm font-medium text-zinc-900">{roleCodes.join(" / ") || "-"}</p>
</div> </div>
<DropdownMenu> <DropdownMenu>
+11 -10
查看文件
@@ -1,23 +1,24 @@
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { ErrorBoundary } from "../components/common/ErrorBoundary";
import { Sidebar } from "../components/navigation/Sidebar"; import { Sidebar } from "../components/navigation/Sidebar";
import { Topbar } from "../components/navigation/Topbar"; import { Topbar } from "../components/navigation/Topbar";
export function AppShellLayout() { export function AppShellLayout() {
return ( return (
<div className="min-h-screen bg-background-light"> <div className="flex min-h-screen bg-zinc-50">
<div className="flex min-h-screen"> <Sidebar />
<Sidebar />
<div className="flex min-w-0 flex-1 flex-col"> <main className="flex-1 flex flex-col min-w-0 lg:ml-64">
<Topbar /> <Topbar />
<main className="flex-1 p-4 sm:p-6"> <div className="flex-1 p-4 sm:p-8">
<div className="layout-container max-w-none"> <div className="layout-container max-w-none">
<ErrorBoundary>
<Outlet /> <Outlet />
</div> </ErrorBoundary>
</main> </div>
</div> </div>
</div> </main>
</div> </div>
); );
} }
+12 -7
查看文件
@@ -3,6 +3,8 @@ import ReactDOM from "react-dom/client";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./auth/AuthContext"; import { AuthProvider } from "./auth/AuthContext";
import { ErrorBoundary } from "./components/common/ErrorBoundary";
import { Toaster } from "./components/ui/sonner";
import { queryClient } from "./query/queryClient"; import { queryClient } from "./query/queryClient";
import { AppRouter } from "./router"; import { AppRouter } from "./router";
import { registerPwaIfEnabled } from "./pwa"; import { registerPwaIfEnabled } from "./pwa";
@@ -12,12 +14,15 @@ registerPwaIfEnabled();
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <ErrorBoundary>
<BrowserRouter> <QueryClientProvider client={queryClient}>
<AuthProvider> <BrowserRouter>
<AppRouter /> <AuthProvider>
</AuthProvider> <AppRouter />
</BrowserRouter> </AuthProvider>
</QueryClientProvider> </BrowserRouter>
</QueryClientProvider>
</ErrorBoundary>
<Toaster />
</React.StrictMode>, </React.StrictMode>,
); );