d79e3baa34
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
import { Link, useNavigate } from 'react-router-dom'
|
|
import { ChevronDown, Settings as SettingsIcon, Bell, Menu } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useAuthStore } from '../../store/authStore'
|
|
import api from '../../lib/api'
|
|
import Breadcrumb from './Breadcrumb'
|
|
|
|
export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
|
|
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-30 bg-white border-b border-gray-200">
|
|
<div className="h-14 flex items-center justify-between px-4">
|
|
{/* 左侧:hamburger(移动端)+ 面包屑 */}
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<button
|
|
onClick={onMenuClick}
|
|
className="md:hidden p-1.5 rounded-md hover:bg-gray-100"
|
|
aria-label="打开菜单"
|
|
>
|
|
<Menu className="w-5 h-5 text-gray-600" />
|
|
</button>
|
|
<Breadcrumb />
|
|
</div>
|
|
|
|
{/* 右侧:通知 + 设置 + 用户菜单 */}
|
|
<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>
|
|
<Link to="/notifications" 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>
|
|
)}
|
|
</Link>
|
|
<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>
|
|
)
|
|
}
|