feat: AIHR 智能人力资源管理系统初始提交

- 员工花名册管理(加密存储、导入导出)
- 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条)
- 社保公积金(多城市配置、版本管理、基数调整)
- 解聘管理(6步流程、证据链、工作交接)
- AI 助手(合同审查、风险预测、RAG 知识库)
- Dashboard 仪表盘
- 设置与通知
This commit is contained in:
selfrelease
2026-07-24 13:53:11 +08:00
commit 0df8aa77d9
109 changed files with 38190 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Building2, ChevronDown, Settings as SettingsIcon, Bell } from 'lucide-react'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../../store/authStore'
import api from '../../lib/api'
import clsx from 'clsx'
const tabs = [
{ path: '/', label: '总览' },
{ path: '/roster', label: '员工管理' },
{ path: '/money', label: '薪税' },
{ path: '/social', label: '社保公积金' },
{ path: '/termination', label: '解聘补偿' },
{ path: '/ai-assistant', label: 'AI顾问' },
]
export default function TopNav() {
const location = useLocation()
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const [menuOpen, setMenuOpen] = useState(false)
const { data: dashboardData } = useQuery<any>({
queryKey: ['dashboard'],
queryFn: async () => {
const res = await api.get('/dashboard') as any
return res.data
},
refetchInterval: 60000,
})
const riskCount = dashboardData?.riskSummary?.pending || 0
return (
<header className="sticky top-0 z-50 bg-white border-b border-gray-200">
<div className="max-w-content mx-auto px-4 h-14 flex items-center gap-4">
<Link to="/" className="flex items-center gap-2 font-bold text-gray-900 shrink-0">
<Building2 className="w-5 h-5 text-primary" />
<span className="hidden sm:inline"></span>
</Link>
<nav className="hidden md:flex items-center gap-1 flex-1">
{tabs.map((tab) => (
<Link
key={tab.path}
to={tab.path}
className={clsx(
'px-3 py-1.5 rounded-md text-sm font-medium transition-colors relative',
location.pathname === tab.path
? 'bg-primary/10 text-primary'
: 'text-gray-600 hover:bg-gray-100',
)}
>
{tab.label}
{tab.path === '/' && riskCount > 0 && (
<span className="absolute -top-1 -right-1 min-w-4 h-4 px-1 bg-danger text-white text-xs rounded-full flex items-center justify-center">
{riskCount > 99 ? '99+' : riskCount}
</span>
)}
</Link>
))}
</nav>
<div className="flex items-center gap-2 shrink-0">
<Link to="/settings" className="p-1.5 rounded-md hover:bg-gray-100" aria-label="设置">
<SettingsIcon className="w-4 h-4 text-gray-600" />
</Link>
<button className="relative p-1.5 rounded-md hover:bg-gray-100" aria-label="通知">
<Bell className="w-4 h-4 text-gray-600" />
{riskCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-4 h-4 px-1 bg-danger text-white text-xs rounded-full flex items-center justify-center">
{riskCount > 99 ? '99+' : riskCount}
</span>
)}
</button>
<div className="relative">
<button
onClick={() => setMenuOpen(!menuOpen)}
className="flex items-center gap-1 px-2 py-1.5 rounded-md hover:bg-gray-100"
aria-expanded={menuOpen}
aria-haspopup="menu"
aria-label="用户菜单"
>
<span className="text-sm text-gray-700 hidden sm:inline">{user?.name || '用户'}</span>
<ChevronDown className="w-4 h-4 text-gray-500" />
</button>
{menuOpen && (
<>
<button className="fixed inset-0 z-10 cursor-default" onClick={() => setMenuOpen(false)} aria-label="关闭菜单" />
<div className="absolute right-0 mt-1 w-40 bg-white rounded-md shadow-lg border border-gray-200 z-20">
<Link
to="/settings"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => setMenuOpen(false)}
>
</Link>
<button
onClick={() => {
logout()
navigate('/login')
}}
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
退
</button>
</div>
</>
)}
</div>
</div>
</div>
</header>
)
}