- 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.
91 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|