Files
Mdeical_Sur_Report/src/components/Sidebar.tsx
admin 750cf4129d Add audit log UI and backend API seeded E2E
- Add Auth Context route role guards so doctors cannot directly enter template management, user management, or audit logs.

- Add Audit Logs page, sidebar entry, frontend audit API client, and API client test.

- Add backend audit log query endpoint with super/admin visibility rules and query filtering.

- Extend PostgreSQL integration tests to cover audit log query permissions.

- Move Playwright E2E away from localStorage seed data to real backend API login and seed helpers.

- Add E2E coverage for route guards and audit log visibility.

- Run Playwright backend on port 3100 and proxy Vite API requests there to avoid local port conflicts.

- Make server:dev use the compiled NestJS server path, avoiding tsx parameter-property injection issues.

- Update README, AGENTS, feature, testing, security, deployment, progress, API, backendization, and auth/user module docs.
2026-05-02 02:04:56 +08:00

91 lines
3.9 KiB
TypeScript

import React from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
FileEdit,
FileText,
Layout,
Users,
Settings,
LogOut,
ShieldCheck,
} from 'lucide-react';
import { User } from '../types';
import { storage } from '../utils/storage';
import { useOptionalAuth } from '../auth/AuthContext';
export default function Sidebar() {
const location = useLocation();
const navigate = useNavigate();
const auth = useOptionalAuth();
const authUser = auth?.user ?? null;
const currentUser = authUser ?? storage.get<User>('currentUser', {} as User);
const logout = async () => {
if (auth) {
await auth.logout();
} else {
storage.remove('currentUser');
}
navigate('/');
};
const navItems = [
{ path: '/dashboard', icon: <LayoutDashboard size={18} />, title: '工作台', roles: ['super', 'admin', 'user'] },
{ path: '/report-editor', icon: <FileEdit size={18} />, title: '图文报告生成', roles: ['super', 'admin', 'user'] },
{ path: '/report-manage', icon: <FileText size={18} />, title: '报告管理', roles: ['super', 'admin', 'user'] },
{ path: '/template-manage', icon: <Layout size={18} />, title: '模板管理', roles: ['super', 'admin'] },
{ path: '/user-manage', icon: <Users size={18} />, title: '用户管理', roles: ['super', 'admin'] },
{ path: '/audit-logs', icon: <ShieldCheck size={18} />, title: '审计日志', roles: ['super', 'admin'] },
{ path: '/system-settings', icon: <Settings size={18} />, title: '系统设置', roles: ['super', 'admin', 'user'] },
];
const filteredNavItems = navItems.filter(item => item.roles.includes(currentUser.role));
const isCollapsed = location.pathname === '/report-editor' || location.pathname === '/template-manage';
return (
<aside className={`${isCollapsed ? 'w-20 px-3' : 'w-60 px-6'} bg-sidebar-bg border-r border-border flex flex-col py-8 shrink-0 h-screen sticky top-0 transition-all`}>
<div className={`flex items-center gap-3 mb-12 h-12 ${isCollapsed ? 'justify-center' : ''}`}>
<img src="/logo_square.png" alt="Logo" className="w-10 h-10 object-contain shrink-0" />
<div className={`font-bold text-lg text-text-main leading-tight ${isCollapsed ? 'hidden' : ''}`}>
<div></div>
<div></div>
</div>
</div>
<nav className="flex flex-col gap-1 flex-1">
{filteredNavItems.map((item) => (
<Link
key={item.path}
to={item.path}
title={item.title}
className={`flex items-center rounded-lg text-sm font-medium transition-all ${
isCollapsed ? 'justify-center px-2 py-2.5' : 'gap-3 px-3 py-2.5'
} ${
location.pathname === item.path
? 'bg-[#EFF6FF] text-accent'
: 'text-text-muted hover:bg-bg hover:text-text-main'
}`}
>
{item.icon}
<span className={isCollapsed ? 'hidden' : ''}>{item.title}</span>
</Link>
))}
</nav>
<div className={`mt-auto pt-5 border-t border-border ${isCollapsed ? 'text-center' : ''}`}>
<div className={`text-[12px] text-text-muted mb-1 ${isCollapsed ? 'hidden' : ''}`}></div>
<div className={`font-semibold text-sm text-text-main mb-4 ${isCollapsed ? 'hidden' : ''}`}>{currentUser.name || '未登录'}</div>
<button
onClick={logout}
title="退出登录"
className={`flex items-center rounded-lg text-sm font-medium text-text-muted hover:bg-red-50 hover:text-red-600 transition-all ${isCollapsed ? 'justify-center w-full px-2 py-2.5' : 'gap-3 px-3 py-2.5 w-full'}`}
>
<LogOut size={18} />
<span className={isCollapsed ? 'hidden' : ''}>退</span>
</button>
</div>
</aside>
);
}