diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 058db8d..a0977d5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useState, useEffect } from 'react' +import { Suspense, useState, useEffect } from 'react' import { Routes, Route, Navigate } from 'react-router-dom' import { Toaster } from 'sonner' import { useAuthStore } from './store/authStore' @@ -11,64 +11,65 @@ import PageContainer from './components/layout/PageContainer' import { CommandPalette } from './components/ui/CommandPalette' import { SkeletonPage } from './components/ui/Skeleton' import ErrorBoundary from './components/ui/ErrorBoundary' +import { lazyRetry } from './lib/lazyRetry' -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 OrgChart = lazy(() => import('./pages/OrgChart')) -const SupportDashboard = lazy(() => import('./pages/support/SupportDashboard')) -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 MyAttendance = lazy(() => import('./pages/portal/MyAttendance')) -const MyLeave = lazy(() => import('./pages/portal/MyLeave')) -const SpecialStatus = lazy(() => import('./pages/SpecialStatus')) -const CompanyFiles = lazy(() => import('./pages/CompanyFiles')) -const LeaveApproval = lazy(() => import('./pages/LeaveApproval')) -const TrainingRecords = lazy(() => import('./pages/roster/TrainingRecords')) -const PerformanceRecords = lazy(() => import('./pages/roster/PerformanceRecords')) -const DisciplinaryRecords = lazy(() => import('./pages/roster/DisciplinaryRecords')) +const Login = lazyRetry(() => import('./pages/auth/Login')) +const Register = lazyRetry(() => import('./pages/auth/Register')) +const ForgotPassword = lazyRetry(() => import('./pages/auth/ForgotPassword')) +const Dashboard = lazyRetry(() => import('./pages/Dashboard')) +const Money = lazyRetry(() => import('./pages/Money')) +const SocialInsurance = lazyRetry(() => import('./pages/SocialInsurance')) +const Roster = lazyRetry(() => import('./pages/Roster')) +const OrgChart = lazyRetry(() => import('./pages/OrgChart')) +const SupportDashboard = lazyRetry(() => import('./pages/support/SupportDashboard')) +const Termination = lazyRetry(() => import('./pages/Termination')) +const AIAssistant = lazyRetry(() => import('./pages/AIAssistant')) +const Settings = lazyRetry(() => import('./pages/Settings')) +const Evidence = lazyRetry(() => import('./pages/Evidence')) +const Policies = lazyRetry(() => import('./pages/Policies')) +const Attendance = lazyRetry(() => import('./pages/Attendance')) +const Templates = lazyRetry(() => import('./pages/Templates')) +const AuditLog = lazyRetry(() => import('./pages/AuditLog')) +const Notifications = lazyRetry(() => import('./pages/Notifications')) +const PortalLogin = lazyRetry(() => import('./pages/portal/PortalLogin')) +const Payslip = lazyRetry(() => import('./pages/portal/Payslip')) +const MyContract = lazyRetry(() => import('./pages/portal/MyContract')) +const Onboarding = lazyRetry(() => import('./pages/portal/Onboarding')) +const ContractConfirm = lazyRetry(() => import('./pages/portal/ContractConfirm')) +const MyPolicies = lazyRetry(() => import('./pages/portal/MyPolicies')) +const AutoLogin = lazyRetry(() => import('./pages/portal/AutoLogin')) +const MedicalPeriodCalculator = lazyRetry(() => import('./pages/tools/MedicalPeriodCalculator')) +const HealthCheck = lazyRetry(() => import('./pages/tools/HealthCheck')) +const AnnualValueReport = lazyRetry(() => import('./pages/tools/AnnualValueReport')) +const CalendarPage = lazyRetry(() => import('./pages/Calendar')) +const MyAttendance = lazyRetry(() => import('./pages/portal/MyAttendance')) +const MyLeave = lazyRetry(() => import('./pages/portal/MyLeave')) +const SpecialStatus = lazyRetry(() => import('./pages/SpecialStatus')) +const CompanyFiles = lazyRetry(() => import('./pages/CompanyFiles')) +const LeaveApproval = lazyRetry(() => import('./pages/LeaveApproval')) +const TrainingRecords = lazyRetry(() => import('./pages/roster/TrainingRecords')) +const PerformanceRecords = lazyRetry(() => import('./pages/roster/PerformanceRecords')) +const DisciplinaryRecords = lazyRetry(() => import('./pages/roster/DisciplinaryRecords')) // Sprint 4-5 新增页面 -const EmployeeHome = lazy(() => import('./pages/portal/EmployeeHome')) -const OnboardingProgress = lazy(() => import('./pages/portal/OnboardingProgress')) -const ResignationApply = lazy(() => import('./pages/portal/ResignationApply')) -const MyEsign = lazy(() => import('./pages/portal/MyEsign')) -const MyRecords = lazy(() => import('./pages/portal/MyRecords')) -const RiskCenter = lazy(() => import('./pages/compliance/RiskCenter')) -const SalaryDashboard = lazy(() => import('./pages/SalaryDashboard')) -const CommercialInsurance = lazy(() => import('./pages/CommercialInsurance')) -const EmployeeBenefits = lazy(() => import('./pages/EmployeeBenefits')) -const ESign = lazy(() => import('./pages/ESign')) -const CommissionBonus = lazy(() => import('./pages/CommissionBonus')) +const EmployeeHome = lazyRetry(() => import('./pages/portal/EmployeeHome')) +const OnboardingProgress = lazyRetry(() => import('./pages/portal/OnboardingProgress')) +const ResignationApply = lazyRetry(() => import('./pages/portal/ResignationApply')) +const MyEsign = lazyRetry(() => import('./pages/portal/MyEsign')) +const MyRecords = lazyRetry(() => import('./pages/portal/MyRecords')) +const RiskCenter = lazyRetry(() => import('./pages/compliance/RiskCenter')) +const SalaryDashboard = lazyRetry(() => import('./pages/SalaryDashboard')) +const CommercialInsurance = lazyRetry(() => import('./pages/CommercialInsurance')) +const EmployeeBenefits = lazyRetry(() => import('./pages/EmployeeBenefits')) +const ESign = lazyRetry(() => import('./pages/ESign')) +const CommissionBonus = lazyRetry(() => import('./pages/CommissionBonus')) // 平台管理端 -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')) +const PlatformLogin = lazyRetry(() => import('./pages/platform/PlatformLogin')) +const PlatformDashboard = lazyRetry(() => import('./pages/platform/PlatformDashboard')) +const PlatformOrgs = lazyRetry(() => import('./pages/platform/PlatformOrgs')) +const PlatformUsers = lazyRetry(() => import('./pages/platform/PlatformUsers')) +const PlatformSidebar = lazyRetry(() => import('./components/layout/PlatformSidebar')) function ProtectedRoute({ children }: { children: React.ReactNode }) { const isAuthenticated = useAuthStore((s) => s.isAuthenticated) diff --git a/frontend/src/lib/lazyRetry.ts b/frontend/src/lib/lazyRetry.ts new file mode 100644 index 0000000..b0076da --- /dev/null +++ b/frontend/src/lib/lazyRetry.ts @@ -0,0 +1,42 @@ +import { lazy, ComponentType } from 'react' + +/** + * 带自动重试的懒加载。 + * + * 部署后旧 chunk 文件名(带 hash)被删除,浏览器若缓存了旧 index.html + * 仍会请求旧 chunk 导致 "Failed to fetch dynamically imported module"。 + * 此时自动刷新页面,让浏览器获取新 index.html 中的新 chunk 引用。 + */ +export function lazyRetry>( + factory: () => Promise<{ default: T } | { [key: string]: T }>, +): React.LazyExoticComponent { + return lazy(async () => { + try { + const m = await factory() + // 统一为 { default: T } 格式 + if ('default' in m) return { default: m.default } + // named export 模式:取第一个导出作为 default + const firstKey = Object.keys(m)[0] + return { default: m[firstKey] } + } catch (err: any) { + // 仅对动态导入失败自动刷新,避免其他错误误触发 + if ( + err && + (err.message?.includes('Failed to fetch dynamically imported module') || + err.message?.includes('Importing a module script failed') || + err.message?.includes('error loading dynamically imported module')) + ) { + // 防止无限刷新:用 sessionStorage 标记,仅刷新一次 + const key = 'lazyRetry:' + (err.message.match(/[\w-]+\.js/)?.[0] || 'unknown') + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1') + window.location.reload() + // 返回一个永不 resolve 的 Promise,等待页面刷新 + return new Promise<{ default: T }>(() => {}) + } + sessionStorage.removeItem(key) + } + throw err + } + }) +} diff --git a/frontend/src/pages/AIAssistant.tsx b/frontend/src/pages/AIAssistant.tsx index 788567b..2f2cc7e 100644 --- a/frontend/src/pages/AIAssistant.tsx +++ b/frontend/src/pages/AIAssistant.tsx @@ -1,13 +1,14 @@ -import { useState, lazy, Suspense } from 'react' +import { useState, Suspense } from 'react' import { Bot, FileSearch, Scale, Sparkles, BookOpen, TrendingUp, Loader2 } from 'lucide-react' import PageGuide from '../components/ui/PageGuide' +import { lazyRetry } from '../lib/lazyRetry' -const ChatTab = lazy(() => import('./ai-assistant/ChatTab').then(m => ({ default: m.ChatTab }))) -const PredictTab = lazy(() => import('./ai-assistant/PredictTab').then(m => ({ default: m.PredictTab }))) -const ReviewTab = lazy(() => import('./ai-assistant/ReviewTab').then(m => ({ default: m.ReviewTab }))) -const CaseTab = lazy(() => import('./ai-assistant/CaseTab').then(m => ({ default: m.CaseTab }))) -const KnowledgeTab = lazy(() => import('./ai-assistant/KnowledgeTab').then(m => ({ default: m.KnowledgeTab }))) -const HRReportTab = lazy(() => import('./ai-assistant/HRReportTab').then(m => ({ default: m.HRReportTab }))) +const ChatTab = lazyRetry(() => import('./ai-assistant/ChatTab').then(m => ({ default: m.ChatTab }))) +const PredictTab = lazyRetry(() => import('./ai-assistant/PredictTab').then(m => ({ default: m.PredictTab }))) +const ReviewTab = lazyRetry(() => import('./ai-assistant/ReviewTab').then(m => ({ default: m.ReviewTab }))) +const CaseTab = lazyRetry(() => import('./ai-assistant/CaseTab').then(m => ({ default: m.CaseTab }))) +const KnowledgeTab = lazyRetry(() => import('./ai-assistant/KnowledgeTab').then(m => ({ default: m.KnowledgeTab }))) +const HRReportTab = lazyRetry(() => import('./ai-assistant/HRReportTab').then(m => ({ default: m.HRReportTab }))) type Tab = 'chat' | 'predict' | 'review' | 'case' | 'knowledge' | 'hr-report' diff --git a/frontend/src/pages/Money.tsx b/frontend/src/pages/Money.tsx index fad6366..0d5566e 100644 --- a/frontend/src/pages/Money.tsx +++ b/frontend/src/pages/Money.tsx @@ -1,12 +1,13 @@ -import { useState, lazy, Suspense } from 'react' +import { useState, Suspense } from 'react' import { useSearchParams } from 'react-router-dom' import { Layers, Wallet, LayoutTemplate, Clock, Receipt, Loader2 } from 'lucide-react' import PageGuide from '../components/ui/PageGuide' +import { lazyRetry } from '../lib/lazyRetry' -const BatchManager = lazy(() => import('./money/BatchTab').then(m => ({ default: m.BatchManager }))) -const TemplateManager = lazy(() => import('./money/TemplateTab').then(m => ({ default: m.TemplateManager }))) -const OvertimeCalculator = lazy(() => import('./money/OvertimeTab').then(m => ({ default: m.OvertimeCalculator }))) -const PayslipManager = lazy(() => import('./money/PayslipTab').then(m => ({ default: m.PayslipManager }))) +const BatchManager = lazyRetry(() => import('./money/BatchTab').then(m => ({ default: m.BatchManager }))) +const TemplateManager = lazyRetry(() => import('./money/TemplateTab').then(m => ({ default: m.TemplateManager }))) +const OvertimeCalculator = lazyRetry(() => import('./money/OvertimeTab').then(m => ({ default: m.OvertimeCalculator }))) +const PayslipManager = lazyRetry(() => import('./money/PayslipTab').then(m => ({ default: m.PayslipManager }))) type Tab = 'batch' | 'template' | 'overtime' | 'payslip'