// 客户端路由 + 应用根组件 // 源码用 DesignCanvas 包一堆 DCArtboard;我们改成 URL-driven,按 location.pathname 选画板。 // 侧栏 是纯展示组件,这里用事件委托拦截 .sb-item[data-nav-id] 的 click 做 pushState。 // 路由映射:path 前缀 -> { navId(侧栏高亮用), crumbs, Page } // navId 必须和 shell.jsx 里 groups.items[*].id 一一对应 const ROUTES = [ { match: (p) => p === "/" || p === "", navId: "dash", crumbs: ["工作台", "总览 Overview"], render: () => }, { match: (p) => p.startsWith("/collect"), navId: "collect", crumbs: ["工作台", "1 · 收集爆款"], render: () => }, { match: (p) => p.startsWith("/workspace"), navId: "teardown", crumbs: ["工作台", "2 · 拆解爆款"], render: () => }, { match: (p) => p.startsWith("/remix"), navId: "remix", crumbs: ["工作台", "3 · 复刻爆款"], render: () => }, { match: (p) => p.startsWith("/qc"), navId: "qc", crumbs: ["工作台", "4 · 质检合成"], render: () => }, { match: (p) => p.startsWith("/scrub"), navId: "scrub", crumbs: ["工作台", "5 · 数据脱敏与扰乱"], render: () => }, { match: (p) => p.startsWith("/distribute"), navId: "distribute", crumbs: ["工作台", "6 · 分发爆款"], render: () => }, { match: (p) => p.startsWith("/library"), navId: "library", crumbs: ["资产", "爆款存储库"], render: () => }, { match: (p) => p.startsWith("/templates") || p.startsWith("/aids"), navId: "templates", crumbs: ["资产", "模板库"], render: () => }, { match: (p) => p.startsWith("/rule-packs"), navId: "rule_packs", crumbs: ["资产", "规则库"], render: () => }, { match: (p) => p.startsWith("/shots-library"), navId: "assets", crumbs: ["资产", "分镜素材库"], render: () => }, { match: (p) => p.startsWith("/image-library"), navId: "image_library", crumbs: ["资产", "图片素材库"], render: () => }, { match: (p) => p.startsWith("/bgm-library"), navId: "bgm_library", crumbs: ["资产", "背景音乐素材库"], render: () => }, { match: (p) => p.startsWith("/accounts"), navId: "accounts", crumbs: ["资产", "账号矩阵"], render: () => }, { match: (p) => p.startsWith("/models"), navId: "models", crumbs: ["系统", "模型管理"], render: () => }, // /login:全屏 iframe SSO 包装页,不显示 Sidebar { match: (p) => p.startsWith("/login"), navId: null, crumbs: ["登录"], chromeless: true, render: () => }, ]; // navId -> 路由路径(侧栏点击时用) const NAV_TO_PATH = { dash: "/", collect: "/collect", teardown: "/workspace", remix: "/remix", qc: "/qc", scrub: "/scrub", distribute: "/distribute", library: "/library", templates: "/templates", rule_packs: "/rule-packs", assets: "/shots-library", image_library: "/image-library", bgm_library: "/bgm-library", accounts: "/accounts", models: "/models", }; function resolveRoute(pathname) { for (const r of ROUTES) { if (r.match(pathname)) return r; } return ROUTES[0]; // fallback to dashboard } // 占位页:4 个规划中的画板通用 function PlaceholderPage({ title, desc }) { return (
COMING SOON
{title}
{desc}
此模块规划中。侧栏保留入口以保持设计稿 1:1 对齐。
); } // 复刻页路由:/remix 默认短视频 tab,/remix?mode=image 切图文 // 这里先简单实现,M2 再加 job_id query 联动原文案 function RemixRouter() { const [mode, setMode] = React.useState(() => { const u = new URL(window.location.href); return u.searchParams.get("mode") === "image" ? "image" : "video"; }); React.useEffect(() => { const onPop = () => { const u = new URL(window.location.href); setMode(u.searchParams.get("mode") === "image" ? "image" : "video"); }; window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); return mode === "image" ? : ; } // AppShell:只负责 Sidebar + main 容器;Topbar 由各画板自己渲染 // (每页 crumbs / 右上按钮各异,留在页面内是设计稿原结构) function AppShell() { const [pathname, setPathname] = React.useState(window.location.pathname); React.useEffect(() => { const onPop = () => setPathname(window.location.pathname); window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); const route = resolveRoute(pathname); // chromeless 路由(如 /login)满屏渲染,不挂 Sidebar if (route.chromeless) { return
{route.render()}
; } return (
{route.render()}
); } // 事件委托:拦截 .sb-item 点击,改 URL 不刷页 // 例外:data-nav-extern 的 item(如分发子服务)走 window.open 到外部独立 URL document.addEventListener("click", (e) => { const item = e.target.closest(".sb-item[data-nav-id]"); if (!item) return; const externUrl = item.dataset.navExtern; if (externUrl) { e.preventDefault(); window.open(externUrl, "_blank", "noopener,noreferrer"); return; } const navId = item.dataset.navId; const path = NAV_TO_PATH[navId]; if (!path) return; if (window.location.pathname === path && window.location.search === "") return; e.preventDefault(); window.history.pushState({}, "", path); window.dispatchEvent(new PopStateEvent("popstate")); }); // 全局 也支持 SPA 跳转(避免整页刷新) document.addEventListener("click", (e) => { const a = e.target.closest("a[href]"); if (!a) return; const href = a.getAttribute("href"); if (!href || !href.startsWith("/") || href.startsWith("//")) return; if (a.target === "_blank" || e.metaKey || e.ctrlKey || e.shiftKey) return; if (href.startsWith("/api/") || href.startsWith("/static/")) return; e.preventDefault(); window.history.pushState({}, "", href); window.dispatchEvent(new PopStateEvent("popstate")); }); ReactDOM.createRoot(document.getElementById("root")).render();