feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Home, Users, Calculator, UserX, Bot, Shield } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const tabs = [
|
||||
{ path: '/', label: '总览', icon: Home },
|
||||
{ path: '/roster', label: '员工', icon: Users },
|
||||
{ path: '/money', label: '薪税', icon: Calculator },
|
||||
{ path: '/social', label: '社保', icon: Shield },
|
||||
{ path: '/termination', label: '解聘', icon: UserX },
|
||||
{ path: '/ai-assistant', label: 'AI', icon: Bot },
|
||||
]
|
||||
|
||||
export default function MobileTabBar() {
|
||||
const location = useLocation()
|
||||
return (
|
||||
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around items-center h-14 z-50">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
const active = location.pathname === tab.path
|
||||
return (
|
||||
<Link
|
||||
key={tab.path}
|
||||
to={tab.path}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-0.5 flex-1 h-full',
|
||||
active ? 'text-primary' : 'text-gray-500',
|
||||
)}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="text-xs">{tab.label}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function PageContainer({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={clsx('max-w-content mx-auto px-4', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user