// app.jsx — SPA router + responsive shell for OneOS数据中台 // hash routes: #/, #/overview, #/detail, #/history, #/playback, #/alarm, #/inbox, #/esg, #/canvas const ROUTES = [ { path: "overview", icon: "map", label: "实时地图", crumbs: ["OneOS数据中台", "实时监控", "总览"], component: "ArtboardOverview" }, { path: "detail", icon: "car", label: "车辆详情", crumbs: ["OneOS数据中台", "实时监控", "单车详情"], component: "ArtboardDetail" }, { path: "history", icon: "history", label: "历史查询", crumbs: ["OneOS数据中台", "数据分析", "历史查询"], component: "ArtboardHistory" }, { path: "playback", icon: "route", label: "轨迹回放", crumbs: ["OneOS数据中台", "数据分析", "轨迹回放"], component: "ArtboardPlayback" }, { path: "alarm", icon: "bell", label: "事件规则", crumbs: ["OneOS数据中台", "事件中心", "规则编排"], component: "ArtboardAlarm" }, { path: "inbox", icon: "inbox", label: "通知中心", crumbs: ["OneOS数据中台", "事件中心", "通知中心"], component: "ArtboardInbox" }, { path: "integration", icon: "plug", label: "数据接入监控", crumbs: ["OneOS数据中台", "数据接入", "监控总览"], component: "ArtboardIntegration" }, ]; const SUB_ROUTES = [ { path: "esg", icon: "chart", label: "ESG·碳减排", crumbs: ["OneOS数据中台", "运营分析", "ESG驾驶舱"], component: "ArtboardESG" }, { path: "canvas", icon: "settings", label: "设计画板", crumbs: ["OneOS数据中台", "设计画板"], 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)}
ZG
); }; // ── 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 }) => (
{title}
); // ── 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;