// app.jsx — SPA router + responsive shell for 羚牛车辆数据中心
// hash routes: #/, #/overview, #/detail, #/history, #/playback, #/alarm, #/inbox, #/esg, #/canvas
const ROUTES = [
{ path: "overview", icon: "map", label: "实时地图", crumbs: ["羚牛车辆数据中心", "实时监控", "总览"], component: "ArtboardOverview" },
{ path: "detail", icon: "car", label: "车辆详情", crumbs: ["羚牛车辆数据中心", "实时监控", "单车详情"], component: "ArtboardDetail" },
{ path: "history", icon: "history", label: "历史查询", crumbs: ["羚牛车辆数据中心", "数据分析", "历史查询"], component: "ArtboardHistory" },
{ path: "playback", icon: "route", label: "轨迹回放", crumbs: ["羚牛车辆数据中心", "数据分析", "轨迹回放"], component: "ArtboardPlayback" },
{ path: "alarm", icon: "bell", label: "事件规则", crumbs: ["羚牛车辆数据中心", "事件中心", "规则编排"], component: "ArtboardAlarm" },
{ path: "inbox", icon: "inbox", label: "通知中心", crumbs: ["羚牛车辆数据中心", "事件中心", "通知中心"], component: "ArtboardInbox" },
];
const SUB_ROUTES = [
{ path: "esg", icon: "chart", label: "ESG·碳减排", crumbs: ["羚牛车辆数据中心", "运营分析", "ESG驾驶舱"], component: "ArtboardESG" },
{ path: "canvas", icon: "settings", label: "设计画板", crumbs: ["羚牛车辆数据中心", "设计画板"], component: "DesignCanvasMode" },
];
const ALL_ROUTES = [...ROUTES, ...SUB_ROUTES];
const DEFAULT_ROUTE = "overview";
// ── Hash router hook ────────────────────────────────────────
const useHashRoute = () => {
const parse = () => {
const h = (window.location.hash || "").replace(/^#\/?/, "").split("?")[0];
return h || DEFAULT_ROUTE;
};
const [route, setRoute] = React.useState(parse());
React.useEffect(() => {
const onHash = () => setRoute(parse());
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
const navigate = (p) => { window.location.hash = "#/" + p; };
return [route, navigate];
};
// ── Viewport hook ───────────────────────────────────────────
const useIsMobile = () => {
const [m, setM] = React.useState(() => window.innerWidth < 900);
React.useEffect(() => {
const on = () => setM(window.innerWidth < 900);
window.addEventListener('resize', on);
return () => window.removeEventListener('resize', on);
}, []);
return m;
};
// ── Responsive sidebar (desktop rail / mobile drawer) ───────
const RouterSidebar = ({ active, onNavigate, isMobile, drawerOpen, onCloseDrawer }) => {
const renderItem = (i) => (
{ onNavigate(i.path); onCloseDrawer && onCloseDrawer(); }}
style={isMobile ? {width:"100%", height:44, display:"flex", justifyContent:"flex-start", padding:"0 16px", gap:14, borderRadius:8} : {}}
>
{isMobile && {i.label}}
);
if (isMobile) {
return (
<>
{drawerOpen && (
)}
{ROUTES.map(renderItem)}
{SUB_ROUTES.map(renderItem)}
>
);
}
return (
onNavigate(DEFAULT_ROUTE)} style={{
cursor:"pointer",
background:"#FFFFFF",
border:"1px solid var(--border-1)",
boxShadow:"0 1px 2px rgba(47,40,40,.06)",
overflow:"hidden",
padding:0,
}}>
{ROUTES.map(renderItem)}
{SUB_ROUTES.map(renderItem)}
);
};
// ── Page wrapper: provides full-bleed canvas + page transition ──
const Page = ({ children, route }) => (
{children}
);
// ── Mobile topbar (replaces desktop topbar on small screens) ──
const MobileTopbar = ({ title, onMenu, onSearch }) => (
);
// ── Canvas mode (preserves the original design board) ────────
const DesignCanvasMode = () => {
const W = 1440, H = 900;
return (
);
};
// ── Component lookup (deferred to render time so window globals are ready) ──
const RESOLVE = (name) => window[name];
// ── Main router app ─────────────────────────────────────────
const RouterApp = () => {
const [route, navigate] = useHashRoute();
const isMobile = useIsMobile();
const [drawerOpen, setDrawerOpen] = React.useState(false);
const meta = ALL_ROUTES.find(r => r.path === route) || ALL_ROUTES[0];
const Cmp = RESOLVE(meta.component) || (() =>
页面 {route} 不存在
);
// close drawer on route change
React.useEffect(() => { setDrawerOpen(false); }, [route]);
// canvas mode = full-bleed, no chrome
if (route === "canvas") {
return (
setDrawerOpen(false)}/>
{isMobile &&
setDrawerOpen(true)}/>}
);
}
// Pages render with their own internal chrome (overview etc include sidebar+topbar inside)
// To avoid double chrome, pages get rendered as full-page content — and we DON'T add an outer shell here.
// Instead, we inject a route-aware Sidebar+Topbar via the `chrome` system.
// SIMPLER: pages own their own .app .sidebar .topbar via the artboard.
// We need to intercept their sidebar to make it clickable.
// So we use a wrapper that overrides chrome by passing context.
// On mobile: render purpose-built MobileRouter with native single-column layouts
if (isMobile && window.MobileRouter && route !== "canvas") {
return (
setDrawerOpen(true) }}>
setDrawerOpen(false)}/>
);
}
return (
setDrawerOpen(true) }}>
{isMobile && (
setDrawerOpen(false)}/>
)}
);
};
// ── Context for child artboards to read route & nav ─────────
const RouteContext = React.createContext({ route: DEFAULT_ROUTE, navigate: () => {}, isMobile: false, openDrawer: () => {} });
const useRoute = () => React.useContext(RouteContext);
window.RouterApp = RouterApp;
window.useRoute = useRoute;
window.RouteContext = RouteContext;
window.ROUTES = ROUTES;
window.SUB_ROUTES = SUB_ROUTES;
window.ALL_ROUTES = ALL_ROUTES;
window.MobileTopbar = MobileTopbar;
window.RouterSidebar = RouterSidebar;