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 ( + + ); +} + 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(" / ") || "-"}

- - 个人设置 - - + + + + + + + + 个人设置 + + + { + 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