Files
TurboHR/frontend/src/components/layout/TopNav.tsx
T
selfrelease 16f22e6622 refactor: 全量迁移前端 API 调用到统一 api-services 服务层
- 新建 api-services-raw.ts 导出原始 axios 方法供特殊端点使用
- 完成 api-services.ts 全领域覆盖(auth/employee/roster/dashboard/attendance/payroll/socialInsurance/commercialInsurance/termination/policies/evidence/audit/calendar/companyFiles/notifications/settings/ai/platform/portal/survey/search)
- 迁移所有 47+ 页面文件:pages/、pages/roster/、pages/portal/、pages/platform/、pages/auth/、pages/dashboard/、pages/compliance/
- 移除所有直接 import api from '../../lib/api' 引用
- 修复 Termination.tsx / WorkProcess.tsx 中 string|null 类型错误
- 修复 SocialInsurance.tsx 中 api-services-raw delete 导入名
- 修复 PlatformLogin.tsx 变量遮蔽问题
- tsc --noEmit 零错误,vite build 成功
2026-08-01 16:13:19 +08:00

119 lines
4.8 KiB
TypeScript

import { Link, useNavigate } from 'react-router-dom'
import { ChevronDown, Settings as SettingsIcon, Bell, Menu, HelpCircle, Smartphone, ClipboardList } from 'lucide-react'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../../store/authStore'
import { dashboardApi } from '../../lib/api-services'
import Breadcrumb from './Breadcrumb'
import HelpModal from '../HelpModal'
import PortalQRModal from '../PortalQRModal'
import SurveyModal from '../SurveyModal'
export default function TopNav({ onMenuClick }: { onMenuClick?: () => void }) {
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const [menuOpen, setMenuOpen] = useState(false)
const [helpOpen, setHelpOpen] = useState(false)
const [portalQROpen, setPortalQROpen] = useState(false)
const [surveyOpen, setSurveyOpen] = useState(false)
const { data: dashboardData } = useQuery<any>({
queryKey: ['dashboard'],
queryFn: () => dashboardApi.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">
<button
onClick={() => setPortalQROpen(true)}
className="p-1.5 rounded-md hover:bg-gray-100"
aria-label="员工端入口"
>
<Smartphone className="w-4 h-4 text-gray-600" />
</button>
<PortalQRModal open={portalQROpen} onClose={() => setPortalQROpen(false)} />
<button
onClick={() => setHelpOpen(true)}
className="p-1.5 rounded-md hover:bg-gray-100"
aria-label="帮助"
>
<HelpCircle className="w-4 h-4 text-gray-600" />
</button>
<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
<button
onClick={() => setSurveyOpen(true)}
className="p-1.5 rounded-md hover:bg-gray-100"
aria-label="功能调查"
>
<ClipboardList className="w-4 h-4 text-gray-600" />
</button>
<SurveyModal open={surveyOpen} onClose={() => setSurveyOpen(false)} />
<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>
)
}