yanhui 9b1ec36c70 fix(router): 补回 BinReplenishMonitor 的 lazy 声明,修复全站白屏
现象:生产 https://ck.yydd.ai:8443 打开任意页面都是白屏,连登录页都进不去。
控制台报 `BinReplenishMonitor is not defined`,一个 /api 请求都没发出去。

根因:PR #74 新增「缺货补货监控」时,在 router/index.jsx 的路由表里写了
`element: renderLazyComponent(BinReplenishMonitor)`,但没有像其它 22 个页面那样
在文件顶部补 `const BinReplenishMonitor = lazy(() => import('../pages/BinReplenishMonitor'))`。
该行在模块顶层求值 → 加载即抛 ReferenceError → 整个 React 应用挂掉。
`vite build` 不会报错(未声明标识符在 JS 里是合法语法),所以一路构建通过、部署后白屏。
PR #75 只补了后端的 binReplenishRoutes 引入,前端的这处漏声明一直没人发现。

修复:
1. router/index.jsx 补上 lazy 声明,并留下注释说明「漏一个就是全站白屏」。
2. 新增 frontend/scripts/verify-router-lazy.mjs,静态扫描 router/index.jsx:
   凡是 renderLazyComponent(X) 用到的 X,必须在同文件有 `const X = lazy(` 声明,
   否则构建直接失败并指出缺少的组件名与行号。已挂到 `npm run build` 前置。

验证:
- 该检查对修复前的 origin/main 版本报错并准确指出
  `BinReplenishMonitor (router/index.jsx:143)`;对修复后版本通过(23 used / 23 declared)。
- eslint 通过;vite build 成功(4270 modules / 34s)。
- playwright 真渲染:修复前 /login 白屏 + pageerror;修复后 /login 完整渲染登录表单
  (123 字、3 个输入框),两个业务路由正常回到登录引导,无致命错误。
2026-10-22 12:06:48 +08:00

233 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { lazy, Suspense } from 'react';
import { createBrowserRouter, Navigate } from 'react-router-dom';
import BasicLayout from '../layouts/BasicLayout';
import { AuthPageLoadingFallback, PageLoadingFallback } from '../components/AppLoadingSkeleton';
const Login = lazy(() => import('../pages/Login'));
const Install = lazy(() => import('../pages/Install'));
const Setup = lazy(() => import('../pages/Setup'));
const Settings = lazy(() => import('../pages/Settings'));
const OnlineDesktopUsers = lazy(() => import('../pages/OnlineDesktopUsers'));
const DesktopExecutionTasks = lazy(() => import('../pages/DesktopExecutionTasks'));
const MarketingEvents = lazy(() => import('../pages/MarketingEvents'));
const ProductInformation = lazy(() => import('../pages/ProductInformation'));
const PromotionModule = lazy(() => import('../pages/PromotionModule'));
const OperationPlaceholder = lazy(() => import('../pages/OperationPlaceholder'));
const DevelopmentStylePreview = lazy(() => import('../pages/DevelopmentStylePreview'));
const ProductAnalysis = lazy(() => import('../pages/ProductAnalysis'));
const OpsBIncentiveBoard = lazy(() => import('../pages/PlanningUpload/OpsBIncentiveBoard'));
const ProductMonitoring = lazy(() => import('../pages/ProductMonitoring'));
const ModularStockMonitor = lazy(() => import('../pages/ModularStockMonitor'));
const MkhBomData = lazy(() => import('../pages/MkhBomData'));
const WarehouseShipTimeliness = lazy(() => import('../pages/WarehouseShipTimeliness'));
const WarehouseLogisticsException = lazy(() => import('../pages/WarehouseLogisticsException'));
const DailyInventory = lazy(() => import('../pages/DailyInventory'));
const StockSalesAnalysis = lazy(() => import('../pages/StockSalesAnalysis'));
const PickingBinSalesAnalysis = lazy(() => import('../pages/PickingBinSalesAnalysis'));
const ReturnsInventoryMonitor = lazy(() => import('../pages/ReturnsInventoryWarning/Monitor'));
// ⚠️ 每个在下面路由表里被 renderLazyComponent(...) 用到的页面都必须先在这里声明。
// 漏一个 = 模块顶层抛 ReferenceError(xxx is not defined)→ 整个应用白屏、连登录页都进不去。
// PR #74 就漏了 BinReplenishMonitor,导致生产 10-22 全站白屏(2026-10-22 修复)。
const BinReplenishMonitor = lazy(() => import('../pages/BinReplenishMonitor'));
const lazyLoadingElement = <PageLoadingFallback />;
const renderLazyComponent = (lazyView, props = {}, fallback = lazyLoadingElement) => {
const LazyView = lazyView;
return (
<Suspense fallback={fallback}>
<LazyView {...props} />
</Suspense>
);
};
const HOME_URL = '/planning/dev-style-preview';
const router = createBrowserRouter([
{
path: '/login',
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
},
{
path: '/callback',
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
},
{
path: '/setup',
element: renderLazyComponent(Setup),
},
{
path: '/install',
element: renderLazyComponent(Install),
},
{
path: '/',
element: <BasicLayout />,
children: [
{
index: true,
element: <Navigate to={HOME_URL} replace />,
},
{
path: 'dashboard',
element: <Navigate to={HOME_URL} replace />,
},
{
path: 'channel',
element: <Navigate to={HOME_URL} replace />,
},
{
path: 'planning',
element: <Navigate to={HOME_URL} replace />,
},
{
path: 'planning/operation/marketing-events',
element: <Navigate to="/planning/operation/marketing-events/activity-products" replace />,
},
{
path: 'planning/operation/marketing-events/activity-products',
element: renderLazyComponent(MarketingEvents),
},
{
path: 'planning/operation/marketing-tools',
element: <Navigate to="/planning/operation/marketing-events" replace />,
},
{
path: 'planning/operation/product-information',
element: renderLazyComponent(ProductInformation),
},
{
path: 'planning/operation/product-analysis',
element: renderLazyComponent(ProductAnalysis),
},
{
path: 'planning/operation/promotion-module',
element: renderLazyComponent(PromotionModule),
},
{
path: 'planning/operation/ops-b-incentive',
element: renderLazyComponent(OpsBIncentiveBoard),
},
{
path: 'planning/operation/product-monitoring',
element: renderLazyComponent(ProductMonitoring),
},
{
path: 'planning/operation/modular-stock-monitor',
element: renderLazyComponent(ModularStockMonitor),
},
{
path: 'planning/operation/mkh-bom-data',
element: renderLazyComponent(MkhBomData),
},
{
path: 'planning/operation/warehouse-ship-timeliness',
element: renderLazyComponent(WarehouseShipTimeliness),
},
{
path: 'planning/operation/warehouse-logistics-exception',
element: renderLazyComponent(WarehouseLogisticsException),
},
{
path: 'planning/operation/daily-inventory',
element: renderLazyComponent(DailyInventory),
},
{
path: 'planning/operation/stock-sales-analysis',
element: renderLazyComponent(StockSalesAnalysis),
},
{
path: 'planning/operation/picking-bin-sales-analysis',
element: renderLazyComponent(PickingBinSalesAnalysis),
},
// 缺货补货监控:拣货位缺货 → 从存货位补货
{
path: 'planning/operation/bin-replenish-monitor',
element: renderLazyComponent(BinReplenishMonitor),
},
// 客退数据监控(5 张看板共用同一个页面,靠 warehouseKey 区分取哪个仓;
// 单一「退货库存预警」视图,原 明细/超期/汇总/差异 四个页签已下线)
{
path: 'planning/operation/returns-inventory/cuiqiao',
element: renderLazyComponent(ReturnsInventoryMonitor, { warehouseKey: 'cuiqiao' }),
},
{
path: 'planning/operation/returns-inventory/subei',
element: renderLazyComponent(ReturnsInventoryMonitor, { warehouseKey: 'subei' }),
},
{
path: 'planning/operation/returns-inventory/suining',
element: renderLazyComponent(ReturnsInventoryMonitor, { warehouseKey: 'suining' }),
},
{
path: 'planning/operation/returns-inventory/shandong',
element: renderLazyComponent(ReturnsInventoryMonitor, { warehouseKey: 'shandong' }),
},
{
path: 'planning/operation/returns-inventory/henan',
element: renderLazyComponent(ReturnsInventoryMonitor, { warehouseKey: 'henan' }),
},
// 历史路径(子看板 / 页签形态)→ 统一回到主看板
{
path: 'planning/operation/returns-inventory/henlin2',
element: <Navigate to="/planning/operation/returns-inventory/cuiqiao" replace />,
},
{
path: 'planning/operation/returns-inventory/henlin2/monitor',
element: <Navigate to="/planning/operation/returns-inventory/cuiqiao" replace />,
},
{
path: 'planning/operation/returns-inventory/henlin2/inbound',
element: <Navigate to="/planning/operation/returns-inventory/cuiqiao" replace />,
},
{
path: 'planning/operation/returns-inventory/henlin2/stock',
element: <Navigate to="/planning/operation/returns-inventory/cuiqiao" replace />,
},
{
path: 'planning/operation/returns-inventory/cuiqiao/monitor',
element: <Navigate to="/planning/operation/returns-inventory/cuiqiao" replace />,
},
{ path: 'planning/operation/store-checkup', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/review-analysis', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/customer-service-checkup', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/pricing-calculator', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/desktop-client-download', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/ai-buyer-show', element: <Navigate to={HOME_URL} replace /> },
{ path: 'planning/operation/ai-seeding-video', element: <Navigate to={HOME_URL} replace /> },
{ path: 'settings/desktop-client-releases', element: <Navigate to={HOME_URL} replace /> },
{
path: 'planning/operation/:featureKey',
element: renderLazyComponent(OperationPlaceholder),
},
{
path: 'planning/upload',
element: <Navigate to={HOME_URL} replace />,
},
{
path: 'planning/dev-style-preview',
element: renderLazyComponent(DevelopmentStylePreview),
},
{
path: 'settings',
element: renderLazyComponent(Settings),
},
{
path: 'settings/online-desktop-users',
element: renderLazyComponent(OnlineDesktopUsers),
},
{
path: 'settings/desktop-execution-tasks',
element: renderLazyComponent(DesktopExecutionTasks),
},
{
path: '*',
element: <Navigate to={HOME_URL} replace />,
},
],
},
]);
export default router;