优化UI
This commit is contained in:
+90
-13
@@ -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<ResolvedRoute>(() => resolveRoute());
|
||||
const page = route.page;
|
||||
@@ -116,6 +135,8 @@ export function App() {
|
||||
const [notice, setNotice] = useState<Notice>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [accountAnchor, setAccountAnchor] = useState<DOMRect | null>(null);
|
||||
// 返回上一页时待恢复的滚动位置;null = 前进导航,滚到顶部
|
||||
const pendingScrollRef = useRef<number | null>(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/<section>。
|
||||
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 <AssetFactoryPage navigate={navigate} />;
|
||||
case "freeCreate":
|
||||
return <FreeCreatePage modelConfigs={modelConfigs} onNotify={(type, text) => setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} onBack={() => navigate("projects")} />;
|
||||
return <FreeCreatePage modelConfigs={modelConfigs} onNotify={(type, text) => setNotice({ type, text })} onTaskSettled={refreshFreeCreateShell} onBack={() => goBack("projects")} />;
|
||||
case "imageOptimize":
|
||||
return <ImageWorkbenchPage mode="image" products={products} modelConfigs={modelConfigs} imageProductId={route.productId} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
return <ImageWorkbenchPage mode="image" products={products} modelConfigs={modelConfigs} imageProductId={route.productId} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
case "modelPhoto":
|
||||
return <ImageWorkbenchPage mode="model" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
return <ImageWorkbenchPage mode="model" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
case "platformCover":
|
||||
return <ImageWorkbenchPage mode="cover" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => navigate("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
return <ImageWorkbenchPage mode="cover" products={products} modelConfigs={modelConfigs} initialProductId={activeProductId} onProductChange={setActiveProductId} unreadByProduct={aiUnreadByProduct} onProductViewed={markAiRead} onBack={() => goBack("assetFactory")} navigate={navigate} onGenerate={generateImages} onResume={resumeImages} onNotify={(type, text) => setNotice({ type, text })} />;
|
||||
case "modelPhotoDemoA":
|
||||
return <ModelPhotoDemoPage variant="A" products={products} onBack={() => navigate("modelPhoto")} navigate={navigate} />;
|
||||
return <ModelPhotoDemoPage variant="A" products={products} onBack={() => goBack("modelPhoto")} navigate={navigate} />;
|
||||
case "modelPhotoDemoB":
|
||||
return <ModelPhotoDemoPage variant="B" products={products} onBack={() => navigate("modelPhoto")} navigate={navigate} />;
|
||||
return <ModelPhotoDemoPage variant="B" products={products} onBack={() => goBack("modelPhoto")} navigate={navigate} />;
|
||||
case "settings":
|
||||
return <SettingsPage user={currentUser} team={currentTeam} preferences={preferences} sessions={sessions} onSavePreferences={savePreferences} onRevokeSession={revokeSession} onRevokeOthers={revokeOtherSessions} onSaveProfile={saveProfile} onChangePassword={changeOwnPassword} onUploadAvatar={uploadOwnAvatar} onResetAvatar={resetOwnAvatar} onNotify={(text) => setNotice({ type: "success", text })} onLogout={logout} />;
|
||||
case "settingsNotify":
|
||||
@@ -1012,7 +1088,7 @@ export function App() {
|
||||
actions={projectDetailError ? (
|
||||
<>
|
||||
<button className="btn btn-primary" type="button" onClick={() => setDetailRetry((n) => n + 1)}>重试</button>
|
||||
<button className="btn btn-ghost" type="button" onClick={() => navigate("projects")}>返回项目列表</button>
|
||||
<button className="btn btn-ghost" type="button" onClick={() => goBack("projects")}>返回</button>
|
||||
</>
|
||||
) : 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}
|
||||
|
||||
Reference in New Issue
Block a user