Files
TurboHR/frontend/src/App.tsx
T
selfrelease 55286819ae fix: HR系统优化批次1 - P0/P1问题修复
P0-3: 修复合同状态判断逻辑,有合同记录但signDate为null时不再误判未签
P0-5: 修复参保城市默认北京问题,导入和预览均改为null
P0-11: 添加全局ErrorBoundary防止白屏,三处布局均包裹
P1-2: 合同附件改为可选,允许先保存再补充上传
P1-7.2: 排班弹窗增加员工搜索(姓名/部门)
P1-8.2: 加班费导入支持Excel(xlsx/xls)格式,兼容中英文列名
P1-9: 社保/公积金基数月度办理支持逐人修改,后端返回recordId
2026-07-30 18:34:36 +08:00

193 lines
11 KiB
TypeScript

import { lazy, Suspense, useState } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { Toaster } from 'sonner'
import { useAuthStore } from './store/authStore'
import TopNav from './components/layout/TopNav'
import SidebarNav from './components/layout/SidebarNav'
import MobileTabBar from './components/layout/MobileTabBar'
import PortalLayout from './components/layout/PortalLayout'
import PageContainer from './components/layout/PageContainer'
import { SkeletonPage } from './components/ui/Skeleton'
import ErrorBoundary from './components/ui/ErrorBoundary'
const Login = lazy(() => import('./pages/auth/Login'))
const Register = lazy(() => import('./pages/auth/Register'))
const ForgotPassword = lazy(() => import('./pages/auth/ForgotPassword'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Money = lazy(() => import('./pages/Money'))
const SocialInsurance = lazy(() => import('./pages/SocialInsurance'))
const Roster = lazy(() => import('./pages/Roster'))
const Termination = lazy(() => import('./pages/Termination'))
const AIAssistant = lazy(() => import('./pages/AIAssistant'))
const Settings = lazy(() => import('./pages/Settings'))
const Evidence = lazy(() => import('./pages/Evidence'))
const Policies = lazy(() => import('./pages/Policies'))
const Attendance = lazy(() => import('./pages/Attendance'))
const Templates = lazy(() => import('./pages/Templates'))
const AuditLog = lazy(() => import('./pages/AuditLog'))
const Notifications = lazy(() => import('./pages/Notifications'))
const PortalLogin = lazy(() => import('./pages/portal/PortalLogin'))
const Payslip = lazy(() => import('./pages/portal/Payslip'))
const MyContract = lazy(() => import('./pages/portal/MyContract'))
const Onboarding = lazy(() => import('./pages/portal/Onboarding'))
const ContractConfirm = lazy(() => import('./pages/portal/ContractConfirm'))
const MyPolicies = lazy(() => import('./pages/portal/MyPolicies'))
const AutoLogin = lazy(() => import('./pages/portal/AutoLogin'))
const MedicalPeriodCalculator = lazy(() => import('./pages/tools/MedicalPeriodCalculator'))
const HealthCheck = lazy(() => import('./pages/tools/HealthCheck'))
const AnnualValueReport = lazy(() => import('./pages/tools/AnnualValueReport'))
const CalendarPage = lazy(() => import('./pages/Calendar'))
const WorkProcess = lazy(() => import('./pages/WorkProcess'))
const MyAttendance = lazy(() => import('./pages/portal/MyAttendance'))
const SpecialStatus = lazy(() => import('./pages/SpecialStatus'))
// 平台管理端
const PlatformLogin = lazy(() => import('./pages/platform/PlatformLogin'))
const PlatformDashboard = lazy(() => import('./pages/platform/PlatformDashboard'))
const PlatformOrgs = lazy(() => import('./pages/platform/PlatformOrgs'))
const PlatformUsers = lazy(() => import('./pages/platform/PlatformUsers'))
const PlatformSidebar = lazy(() => import('./components/layout/PlatformSidebar'))
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
if (!isAuthenticated) return <Navigate to="/login" replace />
return <>{children}</>
}
function PublicRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
if (isAuthenticated) return <Navigate to="/" replace />
return <>{children}</>
}
function AdminLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="flex min-h-screen bg-[#f8f9fb]">
<SidebarNav mobileOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col min-w-0">
<TopNav onMenuClick={() => setSidebarOpen(true)} />
<main className="flex-1 py-6 pb-20 md:pb-6">
<PageContainer>
<ErrorBoundary>
<Suspense fallback={<SkeletonPage />}>{children}</Suspense>
</ErrorBoundary>
</PageContainer>
</main>
<MobileTabBar />
</div>
</div>
)
}
function PlatformRoute({ children }: { children: React.ReactNode }) {
const user = useAuthStore((s) => s.user)
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
if (!isAuthenticated || user?.role !== 'SUPER_ADMIN') return <Navigate to="/platform/login" replace />
return <>{children}</>
}
function PlatformLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="flex min-h-screen bg-[#f8f9fb]">
<PlatformSidebar mobileOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col min-w-0">
<header className="h-14 bg-white border-b border-gray-200 flex items-center px-4 shrink-0">
<button
className="md:hidden mr-3 text-gray-400"
onClick={() => setSidebarOpen(true)}
aria-label="打开菜单"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<span className="text-sm text-gray-500"></span>
<span className="ml-2 px-2 py-0.5 rounded text-[10px] font-bold bg-primary text-white">ADMIN</span>
</header>
<main className="flex-1 py-6 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<ErrorBoundary>
<Suspense fallback={<SkeletonPage />}>{children}</Suspense>
</ErrorBoundary>
</div>
</main>
</div>
</div>
)
}
function PortalLayoutWrapper({ children, showNav = true }: { children: React.ReactNode; showNav?: boolean }) {
if (!showNav) {
return (
<div className="min-h-screen bg-surface pt-safe pb-safe">
<main className="max-w-md mx-auto py-6 px-4">
<ErrorBoundary>
<Suspense fallback={<SkeletonPage />}>{children}</Suspense>
</ErrorBoundary>
</main>
</div>
)
}
return (
<PortalLayout>
<ErrorBoundary>
<Suspense fallback={<SkeletonPage />}>{children}</Suspense>
</ErrorBoundary>
</PortalLayout>
)
}
export default function App() {
return (
<>
<Routes>
{/* 管理端认证页面 */}
<Route path="/login" element={<Suspense fallback={<SkeletonPage />}><PublicRoute><Login /></PublicRoute></Suspense>} />
<Route path="/register" element={<Suspense fallback={<SkeletonPage />}><PublicRoute><Register /></PublicRoute></Suspense>} />
<Route path="/forgot-password" element={<Suspense fallback={<SkeletonPage />}><PublicRoute><ForgotPassword /></PublicRoute></Suspense>} />
{/* 管理端业务页面 */}
<Route path="/" element={<ProtectedRoute><AdminLayout><Dashboard /></AdminLayout></ProtectedRoute>} />
<Route path="/roster" element={<ProtectedRoute><AdminLayout><Roster /></AdminLayout></ProtectedRoute>} />
<Route path="/money" element={<ProtectedRoute><AdminLayout><Money /></AdminLayout></ProtectedRoute>} />
<Route path="/social" element={<ProtectedRoute><AdminLayout><SocialInsurance /></AdminLayout></ProtectedRoute>} />
<Route path="/termination" element={<ProtectedRoute><AdminLayout><Termination /></AdminLayout></ProtectedRoute>} />
<Route path="/ai-assistant" element={<ProtectedRoute><AdminLayout><AIAssistant /></AdminLayout></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><AdminLayout><Settings /></AdminLayout></ProtectedRoute>} />
<Route path="/evidence" element={<ProtectedRoute><AdminLayout><Evidence /></AdminLayout></ProtectedRoute>} />
<Route path="/policies" element={<ProtectedRoute><AdminLayout><Policies /></AdminLayout></ProtectedRoute>} />
<Route path="/attendance" element={<ProtectedRoute><AdminLayout><Attendance /></AdminLayout></ProtectedRoute>} />
<Route path="/calendar" element={<ProtectedRoute><AdminLayout><CalendarPage /></AdminLayout></ProtectedRoute>} />
<Route path="/templates" element={<ProtectedRoute><AdminLayout><Templates /></AdminLayout></ProtectedRoute>} />
<Route path="/audit" element={<ProtectedRoute><AdminLayout><AuditLog /></AdminLayout></ProtectedRoute>} />
<Route path="/notifications" element={<ProtectedRoute><AdminLayout><Notifications /></AdminLayout></ProtectedRoute>} />
<Route path="/tools/medical-period" element={<ProtectedRoute><AdminLayout><MedicalPeriodCalculator /></AdminLayout></ProtectedRoute>} />
<Route path="/tools/health-check" element={<ProtectedRoute><AdminLayout><HealthCheck /></AdminLayout></ProtectedRoute>} />
<Route path="/tools/annual-value" element={<ProtectedRoute><AdminLayout><AnnualValueReport /></AdminLayout></ProtectedRoute>} />
<Route path="/work-process" element={<ProtectedRoute><AdminLayout><WorkProcess /></AdminLayout></ProtectedRoute>} />
<Route path="/special-status" element={<ProtectedRoute><AdminLayout><SpecialStatus /></AdminLayout></ProtectedRoute>} />
{/* 平台管理端 */}
<Route path="/platform/login" element={<Suspense fallback={<SkeletonPage />}><PlatformLogin /></Suspense>} />
<Route path="/platform/dashboard" element={<PlatformRoute><PlatformLayout><PlatformDashboard /></PlatformLayout></PlatformRoute>} />
<Route path="/platform/orgs" element={<PlatformRoute><PlatformLayout><PlatformOrgs /></PlatformLayout></PlatformRoute>} />
<Route path="/platform/users" element={<PlatformRoute><PlatformLayout><PlatformUsers /></PlatformLayout></PlatformRoute>} />
{/* 员工端 */}
<Route path="/portal/login" element={<PortalLayoutWrapper showNav={false}><PortalLogin /></PortalLayoutWrapper>} />
<Route path="/portal/payslip" element={<PortalLayoutWrapper><Payslip /></PortalLayoutWrapper>} />
<Route path="/portal/contract" element={<PortalLayoutWrapper><MyContract /></PortalLayoutWrapper>} />
<Route path="/portal/onboarding" element={<PortalLayoutWrapper showNav={false}><Onboarding /></PortalLayoutWrapper>} />
<Route path="/portal/contract-confirm" element={<PortalLayoutWrapper showNav={false}><ContractConfirm /></PortalLayoutWrapper>} />
<Route path="/portal/policies" element={<PortalLayoutWrapper><MyPolicies /></PortalLayoutWrapper>} />
<Route path="/portal/attendance" element={<PortalLayoutWrapper><MyAttendance /></PortalLayoutWrapper>} />
<Route path="/portal/auto-login" element={<PortalLayoutWrapper showNav={false}><AutoLogin /></PortalLayoutWrapper>} />
{/* 兜底 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<Toaster position="top-center" richColors closeButton />
</>
)
}