From ba1613f2b59a382cf6e442fbc89ff1906afc8cab Mon Sep 17 00:00:00 2001 From: "Azmat@qq.com" Date: Thu, 20 Aug 2026 12:52:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/frontend/src/App.tsx | 103 +++++++++++-- core/frontend/src/account-page.css | 5 + core/frontend/src/ai-tools-page.css | 14 +- core/frontend/src/components/app-shell.tsx | 171 ++++++++++++--------- core/frontend/src/dashboard-page.css | 10 +- core/frontend/src/design-restraint.css | 36 ++--- core/frontend/src/free-create-page.css | 9 ++ core/frontend/src/library-page.css | 9 +- core/frontend/src/messages-page.css | 10 ++ core/frontend/src/models-page.css | 14 +- core/frontend/src/product-create-page.css | 5 + core/frontend/src/product-detail-page.css | 5 + core/frontend/src/products-page.css | 9 +- core/frontend/src/projects-page.css | 13 +- core/frontend/src/routes/account.tsx | 2 + core/frontend/src/routes/ai-tools.tsx | 6 +- core/frontend/src/routes/dashboard.tsx | 2 + core/frontend/src/routes/free-create.tsx | 2 + core/frontend/src/routes/library.tsx | 2 + core/frontend/src/routes/messages.tsx | 2 + core/frontend/src/routes/models.tsx | 2 + core/frontend/src/routes/pipeline.tsx | 3 +- core/frontend/src/routes/products.tsx | 6 + core/frontend/src/routes/projects.tsx | 4 +- core/frontend/src/routes/route-config.ts | 2 +- core/frontend/src/routes/settings.tsx | 2 + core/frontend/src/routes/team.tsx | 2 + core/frontend/src/routes/trash.tsx | 2 + core/frontend/src/settings-page.css | 5 + core/frontend/src/styles.css | 8 +- core/frontend/src/team-page.css | 5 + core/frontend/src/trash-page.css | 5 + 32 files changed, 336 insertions(+), 139 deletions(-) diff --git a/core/frontend/src/App.tsx b/core/frontend/src/App.tsx index 628f35b..5697828 100644 --- a/core/frontend/src/App.tsx +++ b/core/frontend/src/App.tsx @@ -43,7 +43,7 @@ import { TeamPage } from "./routes"; import type { AuthMode, NavigateOptions, Notice, Page, ResolvedRoute } from "./routes/route-config"; -import { isOwnerOnlyPage, pathForPage, resolveRoute } from "./routes/route-config"; +import { isOwnerOnlyPage, parentPage, pathForPage, resolveRoute } from "./routes/route-config"; import { AdminApp } from "./routes/admin/admin-app"; import { TrashPage } from "./routes/trash"; import { ModelsPage } from "./routes/models"; @@ -79,6 +79,25 @@ function saveImgwb(mode: string | undefined, patch: ImgwbSaved) { } } +type NavHistoryState = { + airshelf: 1; + scrollY: number; + tab?: string; + from?: { + page: Page; + productId?: string; + projectId?: string; + tab?: string; + hash?: string; + scrollY: number; + }; +}; + +function readNavState(raw: unknown): NavHistoryState | null { + if (!raw || typeof raw !== "object" || !("airshelf" in raw)) return null; + return raw as NavHistoryState; +} + export function App() { const [route, setRoute] = useState(() => resolveRoute()); const page = route.page; @@ -116,6 +135,8 @@ export function App() { const [notice, setNotice] = useState(null); const [loading, setLoading] = useState(false); const [accountAnchor, setAccountAnchor] = useState(null); + // 返回上一页时待恢复的滚动位置;null = 前进导航,滚到顶部 + const pendingScrollRef = useRef(null); // 右上角 toast 自动消失:成功/信息 3s,错误 5s(此前 notice 不会自己清,导致「提示一直挂着」) useEffect(() => { @@ -281,7 +302,9 @@ export function App() { useEffect(() => { function syncRouteFromHistory() { const next = resolveRoute(); - setRoute(next); + const state = readNavState(window.history.state); + pendingScrollRef.current = state?.scrollY ?? 0; + setRoute({ ...next, tab: state?.tab ?? next.tab }); setAuthMode(next.authMode); if (next.productId !== undefined) setActiveProductId(next.productId); if (next.projectId !== undefined) setActiveProjectId(next.projectId); @@ -290,6 +313,22 @@ export function App() { return () => window.removeEventListener("popstate", syncRouteFromHistory); }, []); + useLayoutEffect(() => { + const y = pendingScrollRef.current; + if (y == null) return; + const restore = () => window.scrollTo({ top: y, behavior: "auto" }); + restore(); + const frame = window.requestAnimationFrame(restore); + const later = window.setTimeout(() => { + restore(); + pendingScrollRef.current = null; + }, 120); + return () => { + window.cancelAnimationFrame(frame); + window.clearTimeout(later); + }; + }, [page, route.projectId, route.productId]); + // 平台后台 gating(身份就绪后): // - 平台超管且无团队:任何非 admin 路由都送进 /admin(否则普通外壳因 !team 永久卡 loading) // - 非超管访问 /admin/*:纠回工作台 @@ -487,15 +526,52 @@ export function App() { 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, tab: options.tab }); + const currentPath = `${window.location.pathname}${window.location.hash}`; const path = `${pathForPage(next, { productId, projectId })}${hash ? `#${hash}` : ""}`; + const prevState = readNavState(window.history.state); + const leaving: NavHistoryState = { + airshelf: 1, + scrollY: window.scrollY || document.documentElement.scrollTop || 0, + tab: route.tab, + from: prevState?.from, + }; + if (!options.replace) { + window.history.replaceState(leaving, "", currentPath); + } + setRoute({ page: next, authMode, productId, projectId, hash, tab: options.tab }); + const arriving: NavHistoryState = { + airshelf: 1, + scrollY: 0, + tab: options.tab, + from: options.replace + ? prevState?.from + : { + page, + productId: route.productId, + projectId: route.projectId, + tab: route.tab, + hash: route.hash, + scrollY: leaving.scrollY, + }, + }; if (`${window.location.pathname}${window.location.hash}` !== path || window.location.search) { const method = options.replace ? "replaceState" : "pushState"; - window.history[method](null, "", path); + window.history[method](arriving, "", path); + } else { + window.history.replaceState(arriving, "", path); } + pendingScrollRef.current = null; window.scrollTo({ top: 0, behavior: "auto" }); } + function goBack(fallback: Page = parentPage(page)) { + if (readNavState(window.history.state)?.from?.page) { + window.history.back(); + return; + } + navigate(fallback, { replace: true }); + } + // 平台超管后台导航:section="" → /admin(概览),否则 /admin/
。 function navigateAdmin(section: string, options: { replace?: boolean } = {}) { const path = section ? `/admin/${section}` : "/admin"; @@ -878,7 +954,7 @@ export function App() { products={products} projects={projects} preselectProductId={activeProductId} - onBack={() => navigate("projects")} + onBack={() => goBack("projects")} onCreate={async (payload) => { const created = await action(() => api.createProject(payload), "项目已创建"); if (created) { @@ -886,7 +962,7 @@ export function App() { // (旧项目的脚本会污染新项目的脚本助手 chat) setProjectDetail(created); setProjects((prev) => (prev.some((p) => p.id === created.id) ? prev : [created, ...prev])); - navigate("pipeline", { projectId: created.id }); + navigate("pipeline", { projectId: created.id, replace: true }); } }} onCreateProduct={(payload) => action(() => api.createProduct(payload), "")} @@ -968,17 +1044,17 @@ export function App() { case "assetFactory": return ; case "freeCreate": - return setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} onBack={() => navigate("projects")} />; + return setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} onBack={() => goBack("projects")} />; case "imageOptimize": - return navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; + return goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; case "modelPhoto": - return navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; + return goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; case "platformCover": - return navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; + return goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />; case "modelPhotoDemoA": - return navigate("modelPhoto")} navigate={navigate} />; + return goBack("modelPhoto")} navigate={navigate} />; case "modelPhotoDemoB": - return navigate("modelPhoto")} navigate={navigate} />; + return goBack("modelPhoto")} navigate={navigate} />; case "settings": return setNotice({ type: "success", text })} onLogout={logout} />; case "settingsNotify": @@ -1012,7 +1088,7 @@ export function App() { actions={projectDetailError ? ( <> - + ) : undefined} /> @@ -1027,6 +1103,7 @@ export function App() { textModels={modelConfigs.filter((m) => m.capability === "text" && m.status === "active")} loading={loading} navigate={navigate} + onBack={() => goBack("projects")} user={currentUser} team={currentTeam} products={products} diff --git a/core/frontend/src/account-page.css b/core/frontend/src/account-page.css index 065cba1..998cb9f 100644 --- a/core/frontend/src/account-page.css +++ b/core/frontend/src/account-page.css @@ -14,6 +14,11 @@ margin: -24px -28px -60px; padding: 52px clamp(40px, 3.2vw, 68px) 48px; + .ac-inner { + width: min(1560px, 100%); + margin: 0 auto; + } + .ac-head { min-height: 76px; display: flex; diff --git a/core/frontend/src/ai-tools-page.css b/core/frontend/src/ai-tools-page.css index a5c44bf..b2c5583 100644 --- a/core/frontend/src/ai-tools-page.css +++ b/core/frontend/src/ai-tools-page.css @@ -18,7 +18,7 @@ width: 100%; max-width: none; box-sizing: border-box; - /* 抵消 .content 的 24/28/60,换成影擎 .main 的左右留白,三张入口卡铺满主区 */ + /* 抵消 .content 的 24/28/60,网格底铺满主区。正文对照影擎 .content:1560 居中 */ margin: -24px -28px -60px; padding: 52px clamp(40px, 3.2vw, 68px) 48px; } @@ -26,6 +26,11 @@ .asset-factory { margin: -28px -24px -48px; } } +.asset-factory .asset-factory-inner { + width: min(1560px, 100%); + margin: 0 auto; +} + .asset-factory .af-page-head { min-height: 76px; display: flex; @@ -320,7 +325,7 @@ .asset-factory .af-media-grid { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr)); gap: 18px; margin-top: 18px; } @@ -469,16 +474,11 @@ margin-bottom: 6px; } -@media (max-width: 1280px) { - .asset-factory .af-media-grid:not(.list) { grid-template-columns: repeat(3, minmax(0, 1fr)); } -} @media (max-width: 1100px) { .asset-factory .af-tools { grid-template-columns: 1fr; } .asset-factory .af-toolbar { flex-wrap: wrap; } - .asset-factory .af-media-grid:not(.list) { grid-template-columns: 1fr 1fr; } } @media (max-width: 720px) { - .asset-factory .af-media-grid:not(.list) { grid-template-columns: 1fr; } .asset-factory .af-media-grid.list .af-media { grid-template-columns: 1fr; } .asset-factory .af-media-grid.list .af-media-thumb { width: 100%; height: auto; aspect-ratio: 16 / 10; } } diff --git a/core/frontend/src/components/app-shell.tsx b/core/frontend/src/components/app-shell.tsx index 29f8d10..cd4cef4 100644 --- a/core/frontend/src/components/app-shell.tsx +++ b/core/frontend/src/components/app-shell.tsx @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react"; import { IconKitSvg } from "./IconKitSvg"; -import { useBodyScrollLock, useOverlayTransition } from "./overlays"; +import { ConfirmModal, useBodyScrollLock, useOverlayTransition } from "./overlays"; import type { Product, Project, Team, User } from "../types"; import type { Notice, Page } from "../routes/route-config"; @@ -165,37 +165,50 @@ export function AccountMenu({ open, anchorRect, onClose, navigate, logout, user, const avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase(); const go = (page: Page) => { onClose(); navigate(page); }; - if (!mounted || !rect) return null; + const [confirmLogout, setConfirmLogout] = useState(false); - return createPortal( -
); } diff --git a/core/frontend/src/routes/free-create.tsx b/core/frontend/src/routes/free-create.tsx index feb442f..521889c 100644 --- a/core/frontend/src/routes/free-create.tsx +++ b/core/frontend/src/routes/free-create.tsx @@ -529,6 +529,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack } return (
+
{onBack ? ( @@ -606,6 +607,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack } onClear={clearInput} onSend={() => void handleSend()} /> +
{detailTask && ( +

成品库

@@ -978,6 +979,7 @@ export function LibraryPage({ onUpload, onDeleteMany }: { onUpload: (formData: F ) : (
{emptyHint}对应创作流程生成后会自动入库
)} +
diff --git a/core/frontend/src/routes/messages.tsx b/core/frontend/src/routes/messages.tsx index 3f3c0d3..fd13313 100644 --- a/core/frontend/src/routes/messages.tsx +++ b/core/frontend/src/routes/messages.tsx @@ -363,6 +363,7 @@ export function MessagesPage({ unreadCount, onMarkRead, onMarkAllRead, onArchive return (
+
+
{toast && (
diff --git a/core/frontend/src/routes/models.tsx b/core/frontend/src/routes/models.tsx index 35e4547..5e755a0 100644 --- a/core/frontend/src/routes/models.tsx +++ b/core/frontend/src/routes/models.tsx @@ -344,6 +344,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: { return (
+

模特库

@@ -448,6 +449,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: { })}
)} +
diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index a4919e6..b5fe6bf 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -500,6 +500,7 @@ export function PipelinePage(props: { project: Project; loading: boolean; navigate: (page: Page, options?: { projectId?: string; productId?: string }) => void; + onBack?: () => void; user: User; team: Team; products: Product[]; @@ -2809,7 +2810,7 @@ export function PipelinePage(props: {
-
diff --git a/core/frontend/src/routes/products.tsx b/core/frontend/src/routes/products.tsx index 18ec2b7..59e6127 100644 --- a/core/frontend/src/routes/products.tsx +++ b/core/frontend/src/routes/products.tsx @@ -207,6 +207,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat return (
+

商品库

@@ -319,6 +320,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat )} +
@@ -473,6 +475,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo return (
+

新建商品

@@ -587,6 +590,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
+
); } @@ -952,6 +956,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na return (
+
{/* 顶部 标题 + 状态 */}
@@ -1336,6 +1341,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na })}
+
setPreview(null)} /> diff --git a/core/frontend/src/routes/projects.tsx b/core/frontend/src/routes/projects.tsx index a5de77d..cf8a976 100644 --- a/core/frontend/src/routes/projects.tsx +++ b/core/frontend/src/routes/projects.tsx @@ -199,7 +199,7 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
-
@@ -566,6 +566,7 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op return (
+

视频创作

@@ -750,6 +751,7 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op )} +
diff --git a/core/frontend/src/routes/route-config.ts b/core/frontend/src/routes/route-config.ts index fd54d9d..c78e829 100644 --- a/core/frontend/src/routes/route-config.ts +++ b/core/frontend/src/routes/route-config.ts @@ -118,7 +118,7 @@ export function isPage(value: string): value is Page { export function parentPage(page: Page): Page { if (["productDetail", "productCreateUpload"].includes(page)) return "products"; - if (page === "projectWizard") return "projects"; + if (["projectWizard", "freeCreate", "pipeline"].includes(page)) return "projects"; if (["imageOptimize", "modelPhoto", "modelPhotoDemoA", "modelPhotoDemoB", "platformCover"].includes(page)) { return "assetFactory"; } diff --git a/core/frontend/src/routes/settings.tsx b/core/frontend/src/routes/settings.tsx index 58f5a23..8028492 100644 --- a/core/frontend/src/routes/settings.tsx +++ b/core/frontend/src/routes/settings.tsx @@ -387,6 +387,7 @@ export function SettingsPage({ return (
+

设置

@@ -682,6 +683,7 @@ export function SettingsPage({
+
{/* 上传头像 modal · 选图 → FormData(file) → onUploadAvatar */} +

团队中心

@@ -580,6 +581,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
+
{/* 设置月限额(团队级)· 预设 pill + 实时剩余 */} +

垃圾桶

@@ -371,6 +372,7 @@ export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProduc ))} )} +