import { useCallback, useEffect, useMemo, useState } from "react"; import { api, getToken, setToken } from "./api"; import { IconKitSvg } from "./components/IconKitSvg"; import type { AITask, Asset, BillingSummary, Ledger, ModelConfig, Product, Project, Team, TeamMember, User } from "./types"; import { CornerMarks, Decorations, Sidebar, ToastLike } from "./components/app-shell"; import { AccountPage, AssetFactoryPage, AuthScreen, Dashboard, ImageWorkbenchPage, LibraryPage, MessagesPage, ModelPhotoDemoPage, PipelinePage, ProductCreateUploadPage, ProductDetailPage, ProductsPage, ProjectWizardPage, ProjectsPage, SettingsPage, TeamPage } from "./routes"; import type { AuthMode, NavigateOptions, Notice, Page, ResolvedRoute } from "./routes/route-config"; import { pathForPage, resolveRoute, routeLabels } from "./routes/route-config"; import { money } from "./routes/stage-config"; const crumbLabels: Partial> = { dashboard: "工作台", products: "商品库", productDetail: "商品详情", productCreateUpload: "新建商品", projects: "视频项目", projectWizard: "新建视频项目", pipeline: "生产管线", library: "资产库", account: "消费", team: "团队", messages: "消息中心", assetFactory: "图片生成", imageOptimize: "图片创作", modelPhoto: "模特上身图", modelPhotoDemoA: "模特图方案 A", modelPhotoDemoB: "模特图方案 B", platformCover: "平台套图", settings: "设置", settingsNotify: "设置" }; export function App() { const [route, setRoute] = useState(() => resolveRoute()); const page = route.page; const [authMode, setAuthMode] = useState(route.authMode); const [authed, setAuthed] = useState(() => Boolean(getToken())); const [booting, setBooting] = useState(() => Boolean(getToken())); const [user, setUser] = useState(null); const [team, setTeam] = useState(null); const [products, setProducts] = useState([]); const [projects, setProjects] = useState([]); const [assets, setAssets] = useState([]); const [teamMembers, setTeamMembers] = useState([]); const [modelConfigs, setModelConfigs] = useState([]); const [aiTasks, setAiTasks] = useState([]); const [billing, setBilling] = useState(null); const [ledgers, setLedgers] = useState([]); const [projectDetail, setProjectDetail] = useState(null); const [activeProductId, setActiveProductId] = useState(route.productId || ""); const [activeProjectId, setActiveProjectId] = useState(route.projectId || ""); const [notice, setNotice] = useState(null); const [loading, setLoading] = useState(false); const activeProject = useMemo( () => projects.find((project) => project.id === activeProjectId) || projects[0], [projects, activeProjectId] ); const activeProduct = useMemo( () => products.find((product) => product.id === activeProductId) || products[0], [products, activeProductId] ); const loadData = useCallback(async () => { const [productData, projectData, assetData, billingData, ledgerData, memberData, modelData, taskData] = await Promise.all([ api.products(), api.projects(), api.assets(), api.billingSummary().catch(() => null), api.ledgers().catch(() => []), api.teamMembers().catch(() => []), api.modelConfigs().catch(() => null), api.aiTasks().catch(() => null) ]); setProducts(productData.results); setProjects(projectData.results); setAssets(assetData.results); setTeamMembers(memberData); setModelConfigs(modelData?.results || []); setAiTasks(taskData?.results || []); if (billingData) setBilling(billingData); setLedgers(ledgerData); setActiveProjectId((current) => current || projectData.results[0]?.id || ""); setActiveProductId((current) => current || productData.results[0]?.id || ""); }, []); // Boot: validate token, hydrate identity + data. useEffect(() => { if (!getToken()) { setBooting(false); return; } let cancelled = false; (async () => { try { const identity = await api.me(); if (cancelled) return; setUser(identity.user); setTeam(identity.team); await loadData(); } catch { setToken(null); if (!cancelled) setAuthed(false); } finally { if (!cancelled) setBooting(false); } })(); return () => { cancelled = true; }; }, [loadData]); // Keep route in sync with browser navigation. useEffect(() => { function syncRouteFromHistory() { const next = resolveRoute(); setRoute(next); setAuthMode(next.authMode); if (next.productId !== undefined) setActiveProductId(next.productId); if (next.projectId !== undefined) setActiveProjectId(next.projectId); } window.addEventListener("popstate", syncRouteFromHistory); return () => window.removeEventListener("popstate", syncRouteFromHistory); }, []); // Load full project detail when entering the pipeline. useEffect(() => { if (!authed || page !== "pipeline" || !activeProjectId) { if (page !== "pipeline") setProjectDetail(null); return; } let cancelled = false; api .project(activeProjectId) .then((detail) => { if (!cancelled) setProjectDetail(detail); }) .catch(() => undefined); return () => { cancelled = true; }; }, [authed, page, activeProjectId]); function navigate(next: Page, options: NavigateOptions = {}) { const productId = options.productId ?? activeProductId; const projectId = options.projectId ?? activeProjectId; if (options.productId !== undefined) setActiveProductId(options.productId); if (options.projectId !== undefined) setActiveProjectId(options.projectId); const hash = options.hash?.replace(/^#/, ""); setRoute({ page: next, authMode, productId, projectId, hash }); const path = `${pathForPage(next, { productId, projectId })}${hash ? `#${hash}` : ""}`; if (`${window.location.pathname}${window.location.hash}` !== path || window.location.search) { const method = options.replace ? "replaceState" : "pushState"; window.history[method](null, "", path); } window.scrollTo({ top: 0, behavior: "auto" }); } async function refreshProjectDetail() { if (!activeProjectId) return; const detail = await api.project(activeProjectId).catch(() => null); if (detail) setProjectDetail(detail); } async function action(work: () => Promise, successText: string): Promise { setLoading(true); setNotice(null); try { const result = await work(); setNotice({ type: "success", text: successText }); await loadData(); await refreshProjectDetail(); return result; } catch (error) { setNotice({ type: "error", text: error instanceof Error ? error.message : "操作失败" }); return null; } finally { setLoading(false); } } function onAuthed(payload: { token: string; user: User; team: Team }) { setToken(payload.token); setUser(payload.user); setTeam(payload.team); setAuthed(true); setBooting(true); loadData().finally(() => setBooting(false)); navigate("dashboard", { replace: true }); } async function logout() { await api.logout().catch(() => undefined); setToken(null); setAuthed(false); setUser(null); setTeam(null); setAuthMode("login"); window.history.replaceState(null, "", "/login"); } // ---- Auth gate ---- if (!authed) { return ( { setAuthMode(next); window.history.pushState(null, "", next === "register" ? "/register" : "/login"); }} onAuthed={onAuthed} /> ); } if (booting || !user || !team) { return (

加载中…

// 正在拉取团队数据
); } const currentUser: User = user; const currentTeam: Team = team; function renderPage() { switch (page) { case "dashboard": return ; case "products": return ( navigate("productDetail", { productId })} onCreate={(payload) => action(() => api.createProduct(payload), "商品已创建")} /> ); case "productCreateUpload": return ( { const created = await action(() => api.createProduct(payload), "商品已创建"); if (created) navigate("productDetail", { productId: created.id }); }} onBack={() => navigate("products")} /> ); case "productDetail": if (!activeProduct) return navigate("productDetail", { productId })} onCreate={(payload) => action(() => api.createProduct(payload), "商品已创建")} />; return ( project.product === activeProduct.id)} navigate={navigate} onUpdate={(payload) => action(() => api.updateProduct(activeProduct.id, payload), "商品已更新")} /> ); case "projects": return ( action(() => api.createProject(payload), "项目已创建")} openPipeline={(projectId) => navigate("pipeline", { projectId })} onDelete={(projectId) => action(() => api.deleteProject(projectId), "项目已删除")} /> ); case "projectWizard": return ( navigate("projects")} onCreate={async (payload) => { const created = await action(() => api.createProject(payload), "项目已创建"); if (created) navigate("pipeline", { projectId: created.id }); }} /> ); case "pipeline": // 有项目时由下方 full-screen 特例渲染;这里只兜底「暂无项目」 return (

暂无项目

// 先创建一个视频项目
); case "library": return action(() => api.uploadAsset(formData), "资产已上传")} />; case "account": return ; case "team": return ; case "messages": return ; case "assetFactory": return ; case "imageOptimize": return navigate("assetFactory")} navigate={navigate} />; case "modelPhoto": return navigate("assetFactory")} navigate={navigate} />; case "platformCover": return navigate("assetFactory")} navigate={navigate} />; case "modelPhotoDemoA": return navigate("modelPhoto")} />; case "modelPhotoDemoB": return navigate("modelPhoto")} />; case "settings": return ; case "settingsNotify": return ; default: return ; } } const avatarChar = (currentTeam.name || currentUser.username || "A").slice(0, 1).toUpperCase(); const here = crumbLabels[page] || routeLabels[page] || "工作台"; // 生产管线 · 全屏 bespoke 布局(自带顶栏 + 步进器),脱离常规 shell const pipelineProject = projectDetail || activeProject; if (page === "pipeline" && pipelineProject) { return ( action(() => api.generateScript(pipelineProject.id, { prompt }), "脚本已生成")} onAdoptScript={(scriptId) => action(() => api.adoptScript(pipelineProject.id, scriptId), "脚本已采用")} onGenerateBaseAsset={(kind, prompt) => action(() => api.generateBaseAsset(pipelineProject.id, { kind, prompt }), "基础资产已生成")} onGenerateStoryboard={(prompt) => action(() => api.generateStoryboard(pipelineProject.id, { prompt }), "故事板已生成")} onSkipStoryboard={() => action(() => api.skipStoryboard(pipelineProject.id), "已跳过故事板")} onSubmitVideo={(segmentId, prompt) => action(() => api.submitVideo(pipelineProject.id, { video_segment_id: segmentId, prompt }), "视频片段已提交")} onPollVideo={(segmentId) => action(() => api.pollVideo(pipelineProject.id, segmentId), "片段状态已刷新")} onSubmitAllVideos={(prompt) => action(async () => { const targets = pipelineProject.video_segments.filter((segment) => !["running", "succeeded"].includes(segment.status)); for (const segment of targets) { await api.submitVideo(pipelineProject.id, { video_segment_id: segment.id, prompt: `${prompt} 第 ${segment.sort_order + 1} 段,时长 ${segment.target_duration_seconds} 秒` }); } return targets.length; }, "60s 多段视频任务已提交") } onPollAllVideos={() => action(async () => { const targets = pipelineProject.video_segments.filter((segment) => ["running", "queued"].includes(segment.status)); for (const segment of targets) { await api.pollVideo(pipelineProject.id, segment.id).catch(() => undefined); } return targets.length; }, "视频片段状态已刷新") } onSubmitExport={() => action(() => api.submitExport(pipelineProject.id), "导出任务已提交")} /> ); } return (
{page === "dashboard" ? ( 工作台 ) : ( <> { event.preventDefault(); navigate("dashboard"); }}>工作台 / {here} )}
navigate("account")}> 余额 {money(billing?.account.balance)}
{avatarChar}
{notice && } {renderPage()}
); }