diff --git a/client/src/components/navigation/NavLinks.tsx b/client/src/components/navigation/NavLinks.tsx new file mode 100644 index 00000000..beabf120 --- /dev/null +++ b/client/src/components/navigation/NavLinks.tsx @@ -0,0 +1,70 @@ +import { NavLink } from "react-router-dom"; +import { useAuth } from "../../auth/AuthContext"; +import { hasAnyPermission } from "../../auth/permissions"; +import { cn } from "../../lib/utils"; + +export type NavItem = { + to: string; + label: string; + anyOf?: string[]; +}; + +export const NAV_ITEMS: NavItem[] = [ + { to: "/dashboard", label: "仪表盘" }, + { + to: "/reimbursements", + label: "我的报销", + anyOf: ["expense.view_own", "expense.view_all"], + }, + { + to: "/workflow/tasks", + label: "待我处理", + anyOf: ["expense.approve"], + }, + { + to: "/invoice-pool", + label: "票据中心", + anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"], + }, + { + to: "/audit-logs", + label: "审计工作台", + anyOf: ["system.audit.view"], + }, + { to: "/settings/profile", label: "个人设置" }, +]; + +export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { + const { permissions } = useAuth(); + + const visibleItems = NAV_ITEMS.filter((item) => { + if (!item.anyOf) { + return true; + } + + return hasAnyPermission(permissions, item.anyOf); + }); + + return ( + + {visibleItems.map((item) => ( + onNavigate?.()} + className={({ isActive }) => + cn( + "block rounded-lg px-3 py-2.5 text-sm font-medium transition-colors", + isActive + ? "bg-primary-50 text-primary" + : "text-slate-700 hover:bg-slate-50 hover:text-slate-900", + ) + } + > + {item.label} + + ))} + + ); +} + diff --git a/client/src/components/navigation/Sidebar.tsx b/client/src/components/navigation/Sidebar.tsx index 4f7c7fd0..847b0744 100644 --- a/client/src/components/navigation/Sidebar.tsx +++ b/client/src/components/navigation/Sidebar.tsx @@ -1,49 +1,6 @@ -import { NavLink } from "react-router-dom"; -import { useAuth } from "../../auth/AuthContext"; -import { hasAnyPermission } from "../../auth/permissions"; - -type NavItem = { - to: string; - label: string; - anyOf?: string[]; -}; - -const NAV_ITEMS: NavItem[] = [ - { to: "/dashboard", label: "仪表盘" }, - { - to: "/reimbursements", - label: "我的报销", - anyOf: ["expense.view_own", "expense.view_all"], - }, - { - to: "/workflow/tasks", - label: "待我处理", - anyOf: ["expense.approve"], - }, - { - to: "/invoice-pool", - label: "票据中心", - anyOf: ["invoice.upload", "invoice.view_own", "invoice.manage"], - }, - { - to: "/audit-logs", - label: "审计工作台", - anyOf: ["system.audit.view"], - }, - { to: "/settings/profile", label: "个人设置" }, -]; +import { NavLinks } from "./NavLinks"; export function Sidebar() { - const { permissions } = useAuth(); - - const visibleItems = NAV_ITEMS.filter((item) => { - if (!item.anyOf) { - return true; - } - - return hasAnyPermission(permissions, item.anyOf); - }); - return ( ); } diff --git a/client/src/components/navigation/Topbar.tsx b/client/src/components/navigation/Topbar.tsx index 488cd37c..3ea67451 100644 --- a/client/src/components/navigation/Topbar.tsx +++ b/client/src/components/navigation/Topbar.tsx @@ -1,16 +1,91 @@ +import { useState } from "react"; import { Link } from "react-router-dom"; import { useAuth } from "../../auth/AuthContext"; +import { Button } from "../ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogTrigger, +} from "../ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { NavLinks } from "./NavLinks"; + +function MenuIcon() { + return ( + + + + ); +} export function Topbar() { const { me, logout } = useAuth(); const roleCodes = me?.roleCodes ?? []; + const [mobileNavOpen, setMobileNavOpen] = useState(false); return ( - + + + + + + + + + + + + + + hyb-oa + + 海音达工作台 + + + + ✕ + + + + + + setMobileNavOpen(false)} /> + + + + + + 欢迎回来 {me?.name ?? "-"} + @@ -19,18 +94,29 @@ export function Topbar() { {roleCodes.join(" / ") || "-"} - - 个人设置 - - { - void logout(); - }} - type="button" - > - 退出登录 - + + + + {me?.name ?? "账号"} + + + + + + 个人设置 + + + { + event.preventDefault(); + void logout(); + }} + > + 退出登录 + + + diff --git a/issues/2026-02-08_21-50-09-p4-trialrun-ready.csv b/issues/2026-02-08_21-50-09-p4-trialrun-ready.csv index 5ca3a396..6b2be912 100644 --- a/issues/2026-02-08_21-50-09-p4-trialrun-ready.csv +++ b/issues/2026-02-08_21-50-09-p4-trialrun-ready.csv @@ -1,7 +1,7 @@ ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes E1,CSV快照收口(试运行上线版),新建P4试运行上线版执行合同快照承接全部前后端与联动工作,新CSV通过校验且依赖关系正确旧快照不改动,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-02-08_21-50-09-p4-trialrun-ready.csv,manual,DONE,DONE,DONE,issues/2026-02-08_21-50-09-p4-trialrun-ready.csv,[],none E2,前端UI组件基线(成熟复用),引入UI primitives与移动端44px触控规范并保持现有token策略,三大业务页可复用组件且typecheck与build通过,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/ui/* | client/src/lib/* | client/src/styles/mobile-touch.css | client/src/index.css | client/tailwind.config.js | client/package.json,"[""E1""]",本地拷贝shadcn风格组件子集并记录来源/许可证与回滚路径 -E3,前端布局导航收敛,统一Sidebar与Topbar交互视觉响应式与权限菜单体验,四角色菜单正确窄屏可用路由守卫无闪烁,"manual: 按server与client README启动联调并验证employee manager finance finance_mgr菜单与守卫",manual,TODO,TODO,TODO,client/src/layouts/AppShellLayout.tsx | client/src/components/navigation/* | client/src/components/common/PageHeader.tsx | client/src/router/*,"[""E2""]",none +E3,前端布局导航收敛,统一Sidebar与Topbar交互视觉响应式与权限菜单体验,四角色菜单正确窄屏可用路由守卫无闪烁,"manual: 按server与client README启动联调并验证employee manager finance finance_mgr菜单与守卫",manual,DONE,TODO,TODO,client/src/layouts/AppShellLayout.tsx | client/src/components/navigation/* | client/src/components/common/PageHeader.tsx | client/src/router/*,"[""E2""]",none E4,待办页精修(列表详情动作),用UI primitives收敛待办列表与详情动作区统一加载空态错误并补data-testid,approve reject return transfer动作可用且动作后列表详情刷新一致,cd client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,client/src/pages/WorkflowTasksPage.tsx | client/src/pages/WorkflowTaskDetailPage.tsx | client/src/api/workflow.ts,"[""E3""]",为E2E补稳定选择器data-testid E5,报销页精修(列表详情联动),收敛报销列表详情展示并优化支付凭证与票据区块一致性并补data-testid,employee可create submit withdraw且finance可pay并填写凭证字段,cd client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,client/src/pages/ReimbursementsPage.tsx | client/src/pages/ReimbursementDetailPage.tsx | client/src/api/reimbursements.ts,"[""E3""]",为E2E补稳定选择器data-testid E6,票据中心精修(OCR与预览),收敛上传列表详情并强化OCR状态展示绑定与签名预览交互并补data-testid,OCR_PROVIDER=mock联调可轮询pending_ocr且ocr_failed展示原因且预览可用,cd client && npm run typecheck && npm run build,manual,TODO,TODO,TODO,client/src/pages/InvoicePoolPage.tsx | client/src/api/invoicePool.ts | client/src/api/http.ts,"[""E3""]",为E2E补稳定选择器data-testid
+ hyb-oa +
海音达工作台
欢迎回来
{me?.name ?? "-"}
{roleCodes.join(" / ") || "-"}