229 lines
7.4 KiB
TypeScript
229 lines
7.4 KiB
TypeScript
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||
import { Truck, Route, Activity, Fuel, BatteryCharging, Receipt, MapPinned } from "lucide-react";
|
||
import { Shell, type ModuleConfig } from "./components/Shell";
|
||
import AuthProvider from "./auth/AuthProvider";
|
||
import { useAuth } from "./auth/useAuth";
|
||
import UnauthorizedPage from "./auth/UnauthorizedPage";
|
||
import { canAccessScheduling, canAccessEnergy } from "./shared/auth/roles";
|
||
import { LoadingState, SkeletonBlock, SurfaceCard } from "./components/ui/surface";
|
||
|
||
const AssetsModule = lazy(() => import("./modules/assets/AssetsModule"));
|
||
const MileageModule = lazy(() => import("./modules/mileage/MileageModule"));
|
||
const SchedulingModule = lazy(() => import("./modules/scheduling/SchedulingModule"));
|
||
const HydrogenModule = lazy(() => import("./modules/energy/HydrogenModule"));
|
||
const ElectricModule = lazy(() => import("./modules/energy/ElectricModule"));
|
||
const EtcModule = lazy(() => import("./modules/energy/EtcModule"));
|
||
const EleImportPage = lazy(() => import("./modules/ele/EleImportPage"));
|
||
const FeedbackAdminPage = lazy(() => import("./modules/admin/FeedbackAdminPage"));
|
||
const VehicleHeatmapModule = lazy(() => import("./modules/vehicle-heatmap/VehicleHeatmapModule"));
|
||
const HydrogenHeatmapModule = lazy(() => import("./modules/hydrogen-heatmap/HydrogenHeatmapModule"));
|
||
|
||
const ASSETS_MODULE: ModuleConfig = {
|
||
id: "assets",
|
||
label: "资产管理",
|
||
icon: Truck,
|
||
component: AssetsModule,
|
||
};
|
||
|
||
const MILEAGE_MODULE: ModuleConfig = {
|
||
id: "mileage",
|
||
label: "里程管理",
|
||
icon: Route,
|
||
component: MileageModule,
|
||
};
|
||
|
||
const VEHICLE_HEATMAP_MODULE: ModuleConfig = {
|
||
id: "vehicle-heatmap",
|
||
label: "车辆",
|
||
icon: MapPinned,
|
||
component: VehicleHeatmapModule,
|
||
};
|
||
|
||
const SCHEDULING_MODULE: ModuleConfig = {
|
||
id: "scheduling",
|
||
label: "智能调度",
|
||
icon: Activity,
|
||
component: SchedulingModule,
|
||
};
|
||
|
||
const HYDROGEN_MODULE: ModuleConfig = {
|
||
id: "hydrogen",
|
||
label: "氢能",
|
||
icon: Fuel,
|
||
component: HydrogenModule,
|
||
};
|
||
|
||
const HYDROGEN_HEATMAP_MODULE: ModuleConfig = {
|
||
id: "hydrogen-heatmap",
|
||
label: "加氢",
|
||
icon: MapPinned,
|
||
component: HydrogenHeatmapModule,
|
||
};
|
||
|
||
const ELECTRIC_MODULE: ModuleConfig = {
|
||
id: "electric",
|
||
label: "电能",
|
||
icon: BatteryCharging,
|
||
component: ElectricModule,
|
||
};
|
||
|
||
const ETC_MODULE: ModuleConfig = {
|
||
id: "etc",
|
||
label: "ETC",
|
||
icon: Receipt,
|
||
component: EtcModule,
|
||
};
|
||
|
||
/**
|
||
* 把旧路径 / 根路径归一化到 `/asset` 或 `/energy` 主路径,
|
||
* 必要时携带 hash 一段定位到具体模块。已是主路径或后台管理页则不动。
|
||
*/
|
||
function normalizePath() {
|
||
if (typeof window === "undefined") return;
|
||
const { pathname, hash, search } = window.location;
|
||
|
||
// 主路径 & 隐藏后台页保持不变
|
||
if (pathname === "/asset" || pathname === "/energy") return;
|
||
if (pathname === "/ele/import" || pathname === "/admin/feedback") return;
|
||
|
||
const legacyMap: Record<string, { path: string; hash?: string }> = {
|
||
"/": { path: "/asset" },
|
||
"/vehicle": { path: "/asset", hash: "assets" },
|
||
"/assets": { path: "/asset", hash: "assets" },
|
||
"/mileage": { path: "/asset", hash: "mileage" },
|
||
"/scheduling": { path: "/asset", hash: "scheduling" },
|
||
};
|
||
|
||
// 未知路径兜底到 /asset(保留原 hash 让 Shell 内部继续解析)
|
||
const target = legacyMap[pathname] ?? { path: "/asset" };
|
||
const finalHash = target.hash ? `#${target.hash}` : hash || "";
|
||
window.history.replaceState(null, "", `${target.path}${search}${finalHash}`);
|
||
}
|
||
|
||
normalizePath();
|
||
|
||
type PathSet = "asset" | "energy";
|
||
|
||
function getPathSet(): PathSet {
|
||
return window.location.pathname === "/energy" ? "energy" : "asset";
|
||
}
|
||
|
||
function getRouteKey(): string {
|
||
if (typeof window === "undefined") return "";
|
||
const path = window.location.pathname;
|
||
const hash = window.location.hash;
|
||
if (
|
||
path === "/ele/import" ||
|
||
hash === "#/ele/import" ||
|
||
hash === "#ele/import"
|
||
)
|
||
return "ele/import";
|
||
if (
|
||
path === "/admin/feedback" ||
|
||
hash === "#/admin/feedback" ||
|
||
hash === "#admin/feedback"
|
||
)
|
||
return "admin/feedback";
|
||
return "";
|
||
}
|
||
|
||
function AuthGate() {
|
||
const { isLoading, isAuthenticated, error, user } = useAuth();
|
||
const [routeKey, setRouteKey] = useState(getRouteKey);
|
||
const [pathSet, setPathSet] = useState<PathSet>(getPathSet);
|
||
|
||
// 监听 hashchange / popstate,让 a href="#/..." 跳转与浏览器前进后退能即时生效
|
||
useEffect(() => {
|
||
const update = () => {
|
||
setRouteKey(getRouteKey());
|
||
setPathSet(getPathSet());
|
||
};
|
||
window.addEventListener("hashchange", update);
|
||
window.addEventListener("popstate", update);
|
||
return () => {
|
||
window.removeEventListener("hashchange", update);
|
||
window.removeEventListener("popstate", update);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
document.title = pathSet === "energy" ? "羚牛氢能-能源BI" : "羚牛氢能-资产BI";
|
||
}, [pathSet]);
|
||
|
||
const modules = useMemo<ModuleConfig[]>(() => {
|
||
if (pathSet === "energy") {
|
||
return [HYDROGEN_MODULE, ELECTRIC_MODULE, ETC_MODULE];
|
||
}
|
||
const heatmapChildren = canAccessEnergy(user?.roles)
|
||
? [HYDROGEN_HEATMAP_MODULE, VEHICLE_HEATMAP_MODULE]
|
||
: [VEHICLE_HEATMAP_MODULE];
|
||
const heatmapModule: ModuleConfig = {
|
||
id: "heatmap",
|
||
label: "热力图",
|
||
icon: MapPinned,
|
||
children: heatmapChildren,
|
||
};
|
||
const result: ModuleConfig[] = [ASSETS_MODULE, MILEAGE_MODULE, heatmapModule];
|
||
if (canAccessScheduling(user?.roles)) result.push(SCHEDULING_MODULE);
|
||
return result;
|
||
}, [pathSet, user?.roles]);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen bg-[var(--app-bg)] p-4 text-slate-800 md:p-8">
|
||
<div className="mx-auto flex max-w-5xl flex-col gap-4">
|
||
<div className="rounded-2xl border border-white/70 bg-white/86 p-5 shadow-[0_18px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl">
|
||
<div className="mb-3 text-[11px] font-black text-blue-600">LN BI ACCESS</div>
|
||
<div className="text-xl font-black text-slate-950">正在进入羚牛氢能 BI</div>
|
||
<div className="mt-2 text-xs font-bold text-slate-400">校验登录态、权限与本地开发环境配置</div>
|
||
</div>
|
||
<SurfaceCard className="p-4">
|
||
<div className="grid gap-3 md:grid-cols-4">
|
||
{[0, 1, 2, 3].map(item => <SkeletonBlock key={item} className="h-24" />)}
|
||
</div>
|
||
<div className="mt-4">
|
||
<LoadingState label="正在验证身份" />
|
||
</div>
|
||
</SurfaceCard>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!isAuthenticated) {
|
||
return <UnauthorizedPage message={error || undefined} />;
|
||
}
|
||
|
||
// 隐藏后端管理页:通过路径或 hash 直接访问,主导航不出现
|
||
if (routeKey === "ele/import") {
|
||
return (
|
||
<Suspense fallback={<LoadingState label="正在加载导入工作台" />}>
|
||
<EleImportPage />
|
||
</Suspense>
|
||
);
|
||
}
|
||
if (routeKey === "admin/feedback") {
|
||
return (
|
||
<Suspense fallback={<LoadingState label="正在加载反馈后台" />}>
|
||
<FeedbackAdminPage />
|
||
</Suspense>
|
||
);
|
||
}
|
||
|
||
// /energy 整组按能源权限控制
|
||
if (pathSet === "energy" && !canAccessEnergy(user?.roles)) {
|
||
return <UnauthorizedPage message="无能源管理模块访问权限" />;
|
||
}
|
||
|
||
// key={pathSet} 让两套底栏切换时 Shell 内部 state 重置,避免残留旧 activeModule
|
||
return <Shell key={pathSet} modules={modules} />;
|
||
}
|
||
|
||
export default function App() {
|
||
return (
|
||
<AuthProvider>
|
||
<AuthGate />
|
||
</AuthProvider>
|
||
);
|
||
}
|