优化UI
This commit is contained in:
+90
-13
@@ -43,7 +43,7 @@ import {
|
|||||||
TeamPage
|
TeamPage
|
||||||
} from "./routes";
|
} from "./routes";
|
||||||
import type { AuthMode, NavigateOptions, Notice, Page, ResolvedRoute } from "./routes/route-config";
|
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 { AdminApp } from "./routes/admin/admin-app";
|
||||||
import { TrashPage } from "./routes/trash";
|
import { TrashPage } from "./routes/trash";
|
||||||
import { ModelsPage } from "./routes/models";
|
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() {
|
export function App() {
|
||||||
const [route, setRoute] = useState<ResolvedRoute>(() => resolveRoute());
|
const [route, setRoute] = useState<ResolvedRoute>(() => resolveRoute());
|
||||||
const page = route.page;
|
const page = route.page;
|
||||||
@@ -116,6 +135,8 @@ export function App() {
|
|||||||
const [notice, setNotice] = useState<Notice>(null);
|
const [notice, setNotice] = useState<Notice>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [accountAnchor, setAccountAnchor] = useState<DOMRect | null>(null);
|
const [accountAnchor, setAccountAnchor] = useState<DOMRect | null>(null);
|
||||||
|
// 返回上一页时待恢复的滚动位置;null = 前进导航,滚到顶部
|
||||||
|
const pendingScrollRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// 右上角 toast 自动消失:成功/信息 3s,错误 5s(此前 notice 不会自己清,导致「提示一直挂着」)
|
// 右上角 toast 自动消失:成功/信息 3s,错误 5s(此前 notice 不会自己清,导致「提示一直挂着」)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -281,7 +302,9 @@ export function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function syncRouteFromHistory() {
|
function syncRouteFromHistory() {
|
||||||
const next = resolveRoute();
|
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);
|
setAuthMode(next.authMode);
|
||||||
if (next.productId !== undefined) setActiveProductId(next.productId);
|
if (next.productId !== undefined) setActiveProductId(next.productId);
|
||||||
if (next.projectId !== undefined) setActiveProjectId(next.projectId);
|
if (next.projectId !== undefined) setActiveProjectId(next.projectId);
|
||||||
@@ -290,6 +313,22 @@ export function App() {
|
|||||||
return () => window.removeEventListener("popstate", syncRouteFromHistory);
|
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(身份就绪后):
|
// 平台后台 gating(身份就绪后):
|
||||||
// - 平台超管且无团队:任何非 admin 路由都送进 /admin(否则普通外壳因 !team 永久卡 loading)
|
// - 平台超管且无团队:任何非 admin 路由都送进 /admin(否则普通外壳因 !team 永久卡 loading)
|
||||||
// - 非超管访问 /admin/*:纠回工作台
|
// - 非超管访问 /admin/*:纠回工作台
|
||||||
@@ -487,15 +526,52 @@ export function App() {
|
|||||||
if (options.productId !== undefined) setActiveProductId(options.productId);
|
if (options.productId !== undefined) setActiveProductId(options.productId);
|
||||||
if (options.projectId !== undefined) setActiveProjectId(options.projectId);
|
if (options.projectId !== undefined) setActiveProjectId(options.projectId);
|
||||||
const hash = options.hash?.replace(/^#/, "");
|
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 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) {
|
if (`${window.location.pathname}${window.location.hash}` !== path || window.location.search) {
|
||||||
const method = options.replace ? "replaceState" : "pushState";
|
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" });
|
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>。
|
// 平台超管后台导航:section="" → /admin(概览),否则 /admin/<section>。
|
||||||
function navigateAdmin(section: string, options: { replace?: boolean } = {}) {
|
function navigateAdmin(section: string, options: { replace?: boolean } = {}) {
|
||||||
const path = section ? `/admin/${section}` : "/admin";
|
const path = section ? `/admin/${section}` : "/admin";
|
||||||
@@ -878,7 +954,7 @@ export function App() {
|
|||||||
products={products}
|
products={products}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
preselectProductId={activeProductId}
|
preselectProductId={activeProductId}
|
||||||
onBack={() => navigate("projects")}
|
onBack={() => goBack("projects")}
|
||||||
onCreate={async (payload) => {
|
onCreate={async (payload) => {
|
||||||
const created = await action(() => api.createProject(payload), "项目已创建");
|
const created = await action(() => api.createProject(payload), "项目已创建");
|
||||||
if (created) {
|
if (created) {
|
||||||
@@ -886,7 +962,7 @@ export function App() {
|
|||||||
// (旧项目的脚本会污染新项目的脚本助手 chat)
|
// (旧项目的脚本会污染新项目的脚本助手 chat)
|
||||||
setProjectDetail(created);
|
setProjectDetail(created);
|
||||||
setProjects((prev) => (prev.some((p) => p.id === created.id) ? prev : [created, ...prev]));
|
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), "")}
|
onCreateProduct={(payload) => action(() => api.createProduct(payload), "")}
|
||||||
@@ -968,17 +1044,17 @@ export function App() {
|
|||||||
case "assetFactory":
|
case "assetFactory":
|
||||||
return <AssetFactoryPage navigate={navigate} />;
|
return <AssetFactoryPage navigate={navigate} />;
|
||||||
case "freeCreate":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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} />;
|
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":
|
case "settingsNotify":
|
||||||
@@ -1012,7 +1088,7 @@ export function App() {
|
|||||||
actions={projectDetailError ? (
|
actions={projectDetailError ? (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-primary" type="button" onClick={() => setDetailRetry((n) => n + 1)}>重试</button>
|
<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}
|
) : undefined}
|
||||||
/>
|
/>
|
||||||
@@ -1027,6 +1103,7 @@ export function App() {
|
|||||||
textModels={modelConfigs.filter((m) => m.capability === "text" && m.status === "active")}
|
textModels={modelConfigs.filter((m) => m.capability === "text" && m.status === "active")}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
navigate={navigate}
|
navigate={navigate}
|
||||||
|
onBack={() => goBack("projects")}
|
||||||
user={currentUser}
|
user={currentUser}
|
||||||
team={currentTeam}
|
team={currentTeam}
|
||||||
products={products}
|
products={products}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
|
|
||||||
|
.ac-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.ac-head {
|
.ac-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 抵消 .content 的 24/28/60,换成影擎 .main 的左右留白,三张入口卡铺满主区 */
|
/* 抵消 .content 的 24/28/60,网格底铺满主区。正文对照影擎 .content:1560 居中 */
|
||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,11 @@
|
|||||||
.asset-factory { margin: -28px -24px -48px; }
|
.asset-factory { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-factory .asset-factory-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-factory .af-page-head {
|
.asset-factory .af-page-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -320,7 +325,7 @@
|
|||||||
|
|
||||||
.asset-factory .af-media-grid {
|
.asset-factory .af-media-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
@@ -469,16 +474,11 @@
|
|||||||
margin-bottom: 6px;
|
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) {
|
@media (max-width: 1100px) {
|
||||||
.asset-factory .af-tools { grid-template-columns: 1fr; }
|
.asset-factory .af-tools { grid-template-columns: 1fr; }
|
||||||
.asset-factory .af-toolbar { flex-wrap: wrap; }
|
.asset-factory .af-toolbar { flex-wrap: wrap; }
|
||||||
.asset-factory .af-media-grid:not(.list) { grid-template-columns: 1fr 1fr; }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 720px) {
|
@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 { grid-template-columns: 1fr; }
|
||||||
.asset-factory .af-media-grid.list .af-media-thumb { width: 100%; height: auto; aspect-ratio: 16 / 10; }
|
.asset-factory .af-media-grid.list .af-media-thumb { width: 100%; height: auto; aspect-ratio: 16 / 10; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
import { AlertCircle, Boxes, Check, ChevronDown, Info, LogOut, Settings, UsersRound } from "lucide-react";
|
||||||
import { IconKitSvg } from "./IconKitSvg";
|
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 { Product, Project, Team, User } from "../types";
|
||||||
import type { Notice, Page } from "../routes/route-config";
|
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 avatar = (team?.name || user.username || "A").slice(0, 1).toUpperCase();
|
||||||
const go = (page: Page) => { onClose(); navigate(page); };
|
const go = (page: Page) => { onClose(); navigate(page); };
|
||||||
if (!mounted || !rect) return null;
|
const [confirmLogout, setConfirmLogout] = useState(false);
|
||||||
|
|
||||||
return createPortal(
|
return (
|
||||||
<div
|
<>
|
||||||
ref={menuRef}
|
{mounted && rect ? createPortal(
|
||||||
className={`shell-account-menu${show ? " show" : ""}`}
|
<div
|
||||||
id="shell-account-menu"
|
ref={menuRef}
|
||||||
role="menu"
|
className={`shell-account-menu${show ? " show" : ""}`}
|
||||||
aria-label="账户菜单"
|
id="shell-account-menu"
|
||||||
style={{ left: pos.left, top: pos.top }}
|
role="menu"
|
||||||
>
|
aria-label="账户菜单"
|
||||||
<div className="shell-account-head">
|
style={{ left: pos.left, top: pos.top }}
|
||||||
<span className="av">{user.avatar_url ? <img src={user.avatar_url} alt="头像" /> : avatar}</span>
|
>
|
||||||
<span>
|
<div className="shell-account-head">
|
||||||
<span className="nm">{team?.name || user.username}</span>
|
<span className="av">{user.avatar_url ? <img src={user.avatar_url} alt="头像" /> : avatar}</span>
|
||||||
<span className="mail">{user.username}</span>
|
<span>
|
||||||
</span>
|
<span className="nm">{team?.name || user.username}</span>
|
||||||
</div>
|
<span className="mail">{user.username}</span>
|
||||||
{ACCOUNT_ITEMS.filter((item) => canManageBilling || (item.act !== "team" && item.act !== "account")).map((item) => (
|
</span>
|
||||||
<button key={item.act} type="button" role="menuitem" onClick={() => go(item.act)}>
|
</div>
|
||||||
<IconKitSvg name={item.icon} />
|
{ACCOUNT_ITEMS.filter((item) => canManageBilling || (item.act !== "team" && item.act !== "account")).map((item) => (
|
||||||
{item.label}
|
<button key={item.act} type="button" role="menuitem" onClick={() => go(item.act)}>
|
||||||
</button>
|
<IconKitSvg name={item.icon} />
|
||||||
))}
|
{item.label}
|
||||||
<div className="sep" />
|
</button>
|
||||||
<button type="button" role="menuitem" onClick={() => { onClose(); logout(); }}>
|
))}
|
||||||
<LogOut size={14} />
|
<div className="sep" />
|
||||||
退出登录
|
<button type="button" role="menuitem" onClick={() => { onClose(); setConfirmLogout(true); }}>
|
||||||
</button>
|
<LogOut size={14} />
|
||||||
</div>,
|
退出登录
|
||||||
document.body
|
</button>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
) : null}
|
||||||
|
<ConfirmModal
|
||||||
|
open={confirmLogout}
|
||||||
|
title="退出当前账号"
|
||||||
|
icon={<LogOut size={16} />}
|
||||||
|
detail="确认后将退出当前设备,再次使用需要重新登录。项目、资产、团队成员与余额数据都会保留。"
|
||||||
|
confirmText="确认退出"
|
||||||
|
onCancel={() => setConfirmLogout(false)}
|
||||||
|
onConfirm={() => { setConfirmLogout(false); logout(); }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,93 +275,107 @@ const MODE_TABS: { id: TopModule; label: string; page: Page }[] = [
|
|||||||
{ id: "video", label: "视频创作", page: "projects" },
|
{ id: "video", label: "视频创作", page: "projects" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 顶栏三按钮滑动黑胶囊 · 对照影擎 .mode-tabs / moveModeIndicator 原样接入:
|
// 顶栏三按钮滑动黑胶囊 · 结构 / 类名 / moveModeIndicator 均按影擎 HTML 原样转写。
|
||||||
// clip-path 只露出三个按钮形状,胶囊在缝里看不见;位移走 transform,不在 React state 里改尺寸。
|
|
||||||
export function ModeTabs({ active, navigate }: { active: TopModule | null; navigate: Navigate }) {
|
export function ModeTabs({ active, navigate }: { active: TopModule | null; navigate: Navigate }) {
|
||||||
const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
const rootRef = useRef<HTMLElement>(null);
|
||||||
const clipRectsRef = useRef<Array<SVGRectElement | null>>([]);
|
|
||||||
const indicatorRef = useRef<HTMLSpanElement>(null);
|
const indicatorRef = useRef<HTMLSpanElement>(null);
|
||||||
const firstPaint = useRef(true);
|
const activeRef = useRef(active);
|
||||||
|
const [visualActive, setVisualActive] = useState(active);
|
||||||
|
activeRef.current = visualActive ?? active;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setVisualActive(active);
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
const moveModeIndicator = (tab: HTMLButtonElement, animate = true) => {
|
const moveModeIndicator = (tab: HTMLButtonElement, animate = true) => {
|
||||||
btnRefs.current.forEach((item, index) => {
|
const root = rootRef.current;
|
||||||
const rect = clipRectsRef.current[index];
|
const modeIndicator = indicatorRef.current;
|
||||||
if (!item || !rect) return;
|
if (!root || !modeIndicator) return;
|
||||||
|
const modeTabs = [...root.querySelectorAll<HTMLButtonElement>(".mode-tab")];
|
||||||
|
const modeMaskRects = [...root.querySelectorAll<SVGRectElement>("#modeButtonMask rect")];
|
||||||
|
modeTabs.forEach((item, index) => {
|
||||||
|
const rect = modeMaskRects[index];
|
||||||
|
if (!rect) return;
|
||||||
rect.setAttribute("x", String(item.offsetLeft));
|
rect.setAttribute("x", String(item.offsetLeft));
|
||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", String(item.offsetWidth));
|
rect.setAttribute("width", String(item.offsetWidth));
|
||||||
rect.setAttribute("height", String(item.offsetHeight));
|
rect.setAttribute("height", String(item.offsetHeight));
|
||||||
});
|
});
|
||||||
const modeIndicator = indicatorRef.current;
|
|
||||||
if (!modeIndicator) return;
|
|
||||||
if (!animate) modeIndicator.style.transition = "none";
|
if (!animate) modeIndicator.style.transition = "none";
|
||||||
|
else {
|
||||||
|
modeIndicator.style.transition = "transform 420ms cubic-bezier(0.22, 1, 0.36, 1), width 320ms cubic-bezier(0.22, 1, 0.36, 1)";
|
||||||
|
}
|
||||||
modeIndicator.style.width = `${tab.offsetWidth}px`;
|
modeIndicator.style.width = `${tab.offsetWidth}px`;
|
||||||
modeIndicator.style.transform = `translateX(${tab.offsetLeft}px)`;
|
modeIndicator.style.transform = `translateX(${tab.offsetLeft}px)`;
|
||||||
if (!animate) {
|
if (!animate) {
|
||||||
void modeIndicator.offsetWidth;
|
void modeIndicator.offsetWidth;
|
||||||
modeIndicator.style.transition = "";
|
modeIndicator.style.transition = "transform 420ms cubic-bezier(0.22, 1, 0.36, 1), width 320ms cubic-bezier(0.22, 1, 0.36, 1)";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeButton = () => {
|
const tabFor = (id: TopModule | null) => {
|
||||||
const idx = active ? MODE_TABS.findIndex((tab) => tab.id === active) : -1;
|
const root = rootRef.current;
|
||||||
return idx >= 0 ? btnRefs.current[idx] : null;
|
if (!root || !id) return null;
|
||||||
|
const idx = MODE_TABS.findIndex((tab) => tab.id === id);
|
||||||
|
return idx >= 0 ? root.querySelectorAll<HTMLButtonElement>(".mode-tab")[idx] ?? null : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const firstPaint = useRef(true);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const tab = activeButton();
|
const tab = tabFor(active);
|
||||||
if (tab) moveModeIndicator(tab, !firstPaint.current);
|
if (tab) moveModeIndicator(tab, !firstPaint.current);
|
||||||
firstPaint.current = false;
|
firstPaint.current = false;
|
||||||
}, [active]);
|
}, [active]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const snap = () => {
|
const snap = () => {
|
||||||
const tab = activeButton();
|
const tab = tabFor(activeRef.current);
|
||||||
if (tab) moveModeIndicator(tab, false);
|
if (tab) moveModeIndicator(tab, false);
|
||||||
};
|
};
|
||||||
window.addEventListener("resize", snap);
|
window.addEventListener("resize", snap);
|
||||||
void document.fonts?.ready.then(snap);
|
void document.fonts?.ready.then(snap);
|
||||||
return () => window.removeEventListener("resize", snap);
|
return () => window.removeEventListener("resize", snap);
|
||||||
}, [active]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<nav
|
||||||
className={`yz-mods${active ? "" : " no-active"}`}
|
ref={rootRef}
|
||||||
role="tablist"
|
className={`mode-tabs${visualActive || active ? "" : " no-active"}`}
|
||||||
aria-label="创作模块"
|
aria-label="工作模式"
|
||||||
>
|
>
|
||||||
<svg className="yz-mod-clip-defs" width="0" height="0" aria-hidden="true" focusable="false">
|
{/* clip-path:url(#id) 必须写在文档内 <style> 里,外部 CSS 文件解析不到这个 SVG id */}
|
||||||
|
<style>{`
|
||||||
|
.mode-tabs .mode-indicator-mask {
|
||||||
|
clip-path: url(#modeButtonMask);
|
||||||
|
-webkit-clip-path: url(#modeButtonMask);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<svg className="mode-clip-defs" width="0" height="0" aria-hidden="true" focusable="false">
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="yzModeButtonMask" clipPathUnits="userSpaceOnUse">
|
<clipPath id="modeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||||
<rect ref={(el) => { clipRectsRef.current[0] = el; }} rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
<rect rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
||||||
<rect ref={(el) => { clipRectsRef.current[1] = el; }} rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
<rect rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
||||||
<rect ref={(el) => { clipRectsRef.current[2] = el; }} rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
<rect rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="yz-mod-indicator-mask" aria-hidden="true">
|
<span className="mode-indicator-mask" aria-hidden="true">
|
||||||
<span
|
<span ref={indicatorRef} className="mode-indicator" id="modeIndicator" />
|
||||||
ref={indicatorRef}
|
|
||||||
className="yz-mod-indicator"
|
|
||||||
style={{ width: 104, transform: "translateX(0px)" }}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
{MODE_TABS.map((tab, index) => (
|
{MODE_TABS.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
ref={(el) => { btnRefs.current[index] = el; }}
|
className={`mode-tab${(visualActive ?? active) === tab.id ? " active" : ""}`}
|
||||||
className={`yz-mod${active === tab.id ? " active" : ""}`}
|
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
data-page={tab.id}
|
||||||
aria-selected={active === tab.id}
|
onClick={(event) => {
|
||||||
onClick={() => {
|
setVisualActive(tab.id);
|
||||||
const btn = btnRefs.current[index];
|
moveModeIndicator(event.currentTarget, true);
|
||||||
if (btn) moveModeIndicator(btn, true);
|
|
||||||
navigate(tab.page);
|
navigate(tab.page);
|
||||||
}}
|
}}
|
||||||
>{tab.label}</button>
|
>{tab.label}</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,17 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 抵消 .content 的 24/28/60,换成影擎 .main 左右留白,工作台铺满主区 */
|
/* 抵消 .content 的 24/28/60,网格底铺满主区。正文对照影擎 .content:1560 居中 */
|
||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.dashboard-page { margin: -28px -24px -48px; }
|
.dashboard-page { margin: -28px -24px -48px; padding: 40px 24px 48px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-page .dashboard-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-page .welcome h1 {
|
.dashboard-page .welcome h1 {
|
||||||
@@ -37,7 +42,6 @@
|
|||||||
.dashboard-page .welcome,
|
.dashboard-page .welcome,
|
||||||
.dashboard-page .creation {
|
.dashboard-page .creation {
|
||||||
width: min(1140px, 100%);
|
width: min(1140px, 100%);
|
||||||
margin-right: auto;
|
|
||||||
}
|
}
|
||||||
.dashboard-page .creation { margin-top: 40px; }
|
.dashboard-page .creation { margin-top: 40px; }
|
||||||
.dashboard-page .creation-head {
|
.dashboard-page .creation-head {
|
||||||
|
|||||||
@@ -779,7 +779,8 @@ main { position: relative; background: #fff; min-width: 0; }
|
|||||||
.crumbs .sep { color: var(--black-alpha-32); }
|
.crumbs .sep { color: var(--black-alpha-32); }
|
||||||
.crumbs .here { color: var(--accent-black); font-weight: 500; }
|
.crumbs .here { color: var(--accent-black); font-weight: 500; }
|
||||||
.crumbs a:hover { color: var(--accent-black); }
|
.crumbs a:hover { color: var(--accent-black); }
|
||||||
.topbar .yz-mods {
|
/* 顶栏工作模式 · 影擎 .mode-tabs / .mode-tab / moveModeIndicator 原样 */
|
||||||
|
.topbar .mode-tabs {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -789,25 +790,23 @@ main { position: relative; background: #fff; min-width: 0; }
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-height: 44px;
|
|
||||||
}
|
}
|
||||||
.topbar .yz-mods.no-active .yz-mod-indicator-mask { opacity: 0; }
|
.topbar .mode-tabs.no-active .mode-indicator-mask { opacity: 0; }
|
||||||
.yz-mod-clip-defs {
|
.mode-clip-defs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
.yz-mod-indicator-mask {
|
.mode-indicator-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
clip-path: url(#yzModeButtonMask);
|
|
||||||
-webkit-clip-path: url(#yzModeButtonMask);
|
|
||||||
transition: opacity 180ms ease;
|
transition: opacity 180ms ease;
|
||||||
}
|
}
|
||||||
.yz-mod-indicator {
|
.mode-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -817,43 +816,38 @@ main { position: relative; background: #fff; min-width: 0; }
|
|||||||
background: #101012;
|
background: #101012;
|
||||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.16);
|
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.16);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
transform: translateX(0px);
|
||||||
transform-origin: left center;
|
transform-origin: left center;
|
||||||
transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1), width 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1), width 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
will-change: transform, width;
|
will-change: transform, width;
|
||||||
}
|
}
|
||||||
.topbar .yz-mods .yz-mod {
|
.topbar .mode-tabs .mode-tab {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 104px;
|
min-width: 104px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 0 22px;
|
padding: 0 22px;
|
||||||
gap: 0;
|
|
||||||
border: 1px solid #d9dde3;
|
border: 1px solid #d9dde3;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
color: #2b2e33;
|
color: #2b2e33;
|
||||||
background: rgba(31, 39, 51, 0.025);
|
background: rgba(31, 39, 51, 0.025);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
transition: background 180ms ease, color 180ms ease, border-color 180ms ease;
|
transition: background 180ms ease, color 180ms ease, border-color 180ms ease;
|
||||||
}
|
}
|
||||||
.topbar .yz-mods .yz-mod:hover:not(.active) { background: rgba(31, 39, 51, 0.06); color: #2b2e33; }
|
.topbar .mode-tabs .mode-tab.active {
|
||||||
.topbar .yz-mods .yz-mod.active {
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.topbar .yz-mods .yz-mod.active:hover { background: transparent; color: #fff; }
|
@media (max-width: 1500px) {
|
||||||
|
.topbar .mode-tabs { gap: 9px; }
|
||||||
|
.topbar .mode-tabs .mode-tab { min-width: 88px; padding: 0 16px; }
|
||||||
|
}
|
||||||
.top-search {
|
.top-search {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2883,8 +2877,6 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
|||||||
.dash-funds-mono:hover { color: var(--heat); }
|
.dash-funds-mono:hover { color: var(--heat); }
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.yz-mod-indicator,
|
|
||||||
.yz-mod-indicator-mask,
|
|
||||||
.modal-bg,
|
.modal-bg,
|
||||||
.modal,
|
.modal,
|
||||||
.drawer,
|
.drawer,
|
||||||
|
|||||||
@@ -30,6 +30,15 @@
|
|||||||
.fc-page { margin: -28px -24px -48px; }
|
.fc-page { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fc-page .fc-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.fc-page .fc-head {
|
.fc-page .fc-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
.library-page { margin: -28px -24px -48px; }
|
.library-page { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.library-page .lib-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.library-page .lib-head {
|
.library-page .lib-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -410,8 +415,10 @@
|
|||||||
.library-page .lib-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
.library-page .lib-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
.library-page .list-pager { margin-top: 20px; }
|
.library-page .list-pager { margin-top: 20px; }
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1500px) {
|
||||||
.library-page .lib-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
.library-page .lib-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
.library-page .lib-sel-bar { left: 50%; }
|
.library-page .lib-sel-bar { left: 50%; }
|
||||||
.library-page .lib-toolbar { flex-wrap: wrap; }
|
.library-page .lib-toolbar { flex-wrap: wrap; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
.msg-page { margin: -28px -24px -48px; }
|
.msg-page { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.msg-page .msg-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.msg-page .page-head {
|
.msg-page .page-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
.models-page { margin: -28px -24px -48px; }
|
.models-page { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.models-page .ml-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.models-page .ml-head {
|
.models-page .ml-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -105,8 +110,8 @@
|
|||||||
|
|
||||||
.models-page .ml-grid {
|
.models-page .ml-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 14px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
.models-page .ml-card {
|
.models-page .ml-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -254,11 +259,10 @@
|
|||||||
.models-page .ml-sel-confirm { color: #fff; background: #002fa7; }
|
.models-page .ml-sel-confirm { color: #fff; background: #002fa7; }
|
||||||
.models-page .ml-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
.models-page .ml-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1500px) {
|
||||||
.models-page .ml-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
}
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
.models-page .ml-sel-bar { left: 50%; }
|
.models-page .ml-sel-bar { left: 50%; }
|
||||||
}
|
}
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
全部规则 scope 在 .product-create-page 下,避免污染他页。
|
全部规则 scope 在 .product-create-page 下,避免污染他页。
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
|
.product-create-page .pc-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── 主表单双栏 ─── */
|
/* ─── 主表单双栏 ─── */
|
||||||
.product-create-page .form-grid {
|
.product-create-page .form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-detail-page {
|
.product-detail-page {
|
||||||
|
.pd-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── 顶部 标题 + 状态 ─── */
|
/* ─── 顶部 标题 + 状态 ─── */
|
||||||
.pd-title {
|
.pd-title {
|
||||||
display: flex; align-items: center; gap: 12px;
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
|
|
||||||
|
.pl-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.pl-head {
|
.pl-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -447,9 +452,11 @@
|
|||||||
.list-pager { margin-top: 20px; }
|
.list-pager { margin-top: 20px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
.products-page .pl-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.products-page { margin: -28px -24px -48px; }
|
.products-page { margin: -28px -24px -48px; }
|
||||||
.products-page .pl-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
}
|
}
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.products-page .pl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.products-page .pl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
.projects-page { margin: -28px -24px -48px; }
|
.projects-page { margin: -28px -24px -48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projects-page .projects-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.projects-page .vc-head {
|
.projects-page .vc-head {
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -288,7 +293,8 @@
|
|||||||
|
|
||||||
.projects-page .vc-grid {
|
.projects-page .vc-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
/* 列宽贴筛选条搜索框(~250px),宽屏加列而不是把卡片拉超大 */
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
@@ -504,18 +510,13 @@
|
|||||||
border-color: var(--klein);
|
border-color: var(--klein);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
|
||||||
.projects-page .vc-grid:not(.list) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
||||||
}
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.projects-page .vc-tools.two,
|
.projects-page .vc-tools.two,
|
||||||
.projects-page .vc-tools.three { grid-template-columns: 1fr; }
|
.projects-page .vc-tools.three { grid-template-columns: 1fr; }
|
||||||
.projects-page .vc-toolbar { flex-wrap: wrap; }
|
.projects-page .vc-toolbar { flex-wrap: wrap; }
|
||||||
.projects-page .vc-grid:not(.list) { grid-template-columns: 1fr 1fr; }
|
|
||||||
.projects-page .vc-sel-bar { left: 50%; }
|
.projects-page .vc-sel-bar { left: 50%; }
|
||||||
}
|
}
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.projects-page .vc-grid:not(.list) { grid-template-columns: 1fr; }
|
|
||||||
.projects-page .vc-search { width: 100%; }
|
.projects-page .vc-search { width: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="account-page">
|
<section className="account-page">
|
||||||
|
<div className="ac-inner">
|
||||||
<header className="ac-head">
|
<header className="ac-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>账单库</h1>
|
<h1>账单库</h1>
|
||||||
@@ -617,6 +618,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TopupModal
|
<TopupModal
|
||||||
open={topupChannel !== null}
|
open={topupChannel !== null}
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="asset-factory">
|
<div className="asset-factory">
|
||||||
|
<div className="asset-factory-inner">
|
||||||
<header className="af-page-head">
|
<header className="af-page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>图片创作</h1>
|
<h1>图片创作</h1>
|
||||||
@@ -384,6 +385,7 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Pager page={taskCurPage} total={visible.length} pageSize={TASKS_PER_PAGE} onChange={setTaskPage} />
|
<Pager page={taskCurPage} total={visible.length} pageSize={TASKS_PER_PAGE} onChange={setTaskPage} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 批次详情弹窗:展示该批生成的全部图片(参考资产库),点图放大 */}
|
{/* 批次详情弹窗:展示该批生成的全部图片(参考资产库),点图放大 */}
|
||||||
{openBatch && createPortal(
|
{openBatch && createPortal(
|
||||||
@@ -1600,7 +1602,7 @@ export function ImageWorkbenchPage({
|
|||||||
<div className="yz-image image-workbench">
|
<div className="yz-image image-workbench">
|
||||||
<header className="image-subpage-header">
|
<header className="image-subpage-header">
|
||||||
<div className="image-title-row">
|
<div className="image-title-row">
|
||||||
<button type="button" className="image-back-button" aria-label="返回图片创作" onClick={onBack}>
|
<button type="button" className="image-back-button" aria-label="返回" onClick={onBack}>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
@@ -1811,7 +1813,7 @@ export function ImageWorkbenchPage({
|
|||||||
<div className="yz-image image-workbench">
|
<div className="yz-image image-workbench">
|
||||||
<header className="image-subpage-header">
|
<header className="image-subpage-header">
|
||||||
<div className="image-title-row">
|
<div className="image-title-row">
|
||||||
<button type="button" className="image-back-button" aria-label="返回图片创作" onClick={onBack}>
|
<button type="button" className="image-back-button" aria-label="返回" onClick={onBack}>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export function Dashboard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="dashboard-page">
|
<section className="dashboard-page">
|
||||||
|
<div className="dashboard-inner">
|
||||||
<section className="welcome">
|
<section className="welcome">
|
||||||
<h1>欢迎回来{greetName ? `,${greetName}` : ""}</h1>
|
<h1>欢迎回来{greetName ? `,${greetName}` : ""}</h1>
|
||||||
<p>{dateLabel} · 你有{running}个项目正在进行中</p>
|
<p>{dateLabel} · 你有{running}个项目正在进行中</p>
|
||||||
@@ -258,6 +259,7 @@ export function Dashboard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fc-page">
|
<div className="fc-page">
|
||||||
|
<div className="fc-inner">
|
||||||
<header className="fc-head">
|
<header className="fc-head">
|
||||||
<div className="fc-title-row">
|
<div className="fc-title-row">
|
||||||
{onBack ? (
|
{onBack ? (
|
||||||
@@ -606,6 +607,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
|
|||||||
onClear={clearInput}
|
onClear={clearInput}
|
||||||
onSend={() => void handleSend()}
|
onSend={() => void handleSend()}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{detailTask && (
|
{detailTask && (
|
||||||
<VideoDetailModal
|
<VideoDetailModal
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ export function LibraryPage({ onUpload, onDeleteMany }: { onUpload: (formData: F
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`library-page${editMode ? " manage-mode" : ""}`}>
|
<section className={`library-page${editMode ? " manage-mode" : ""}`}>
|
||||||
|
<div className="lib-inner">
|
||||||
<header className="lib-head">
|
<header className="lib-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>成品库</h1>
|
<h1>成品库</h1>
|
||||||
@@ -978,6 +979,7 @@ export function LibraryPage({ onUpload, onDeleteMany }: { onUpload: (formData: F
|
|||||||
) : (
|
) : (
|
||||||
<div className="lib-empty" role="status"><Images /><strong>{emptyHint}</strong><span>对应创作流程生成后会自动入库</span></div>
|
<div className="lib-empty" role="status"><Images /><strong>{emptyHint}</strong><span>对应创作流程生成后会自动入库</span></div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`lib-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
<div className={`lib-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||||
<div className="lib-sel-copy">
|
<div className="lib-sel-copy">
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ export function MessagesPage({ unreadCount, onMarkRead, onMarkAllRead, onArchive
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="msg-page">
|
<div className="msg-page">
|
||||||
|
<div className="msg-inner">
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<button className="msg-back" type="button" onClick={() => navigate("dashboard")} aria-label="返回">
|
<button className="msg-back" type="button" onClick={() => navigate("dashboard")} aria-label="返回">
|
||||||
<ArrowLeft size={18} strokeWidth={1.75} />
|
<ArrowLeft size={18} strokeWidth={1.75} />
|
||||||
@@ -537,6 +538,7 @@ export function MessagesPage({ unreadCount, onMarkRead, onMarkAllRead, onArchive
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{toast && (
|
{toast && (
|
||||||
<div className="toast show" role="status" aria-live="polite">
|
<div className="toast show" role="status" aria-live="polite">
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`models-page${editMode ? " manage-mode" : ""}`}>
|
<section className={`models-page${editMode ? " manage-mode" : ""}`}>
|
||||||
|
<div className="ml-inner">
|
||||||
<header className="ml-head">
|
<header className="ml-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>模特库</h1>
|
<h1>模特库</h1>
|
||||||
@@ -448,6 +449,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`ml-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
<div className={`ml-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||||
<div className="ml-sel-copy">
|
<div className="ml-sel-copy">
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ export function PipelinePage(props: {
|
|||||||
project: Project;
|
project: Project;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
navigate: (page: Page, options?: { projectId?: string; productId?: string }) => void;
|
navigate: (page: Page, options?: { projectId?: string; productId?: string }) => void;
|
||||||
|
onBack?: () => void;
|
||||||
user: User;
|
user: User;
|
||||||
team: Team;
|
team: Team;
|
||||||
products: Product[];
|
products: Product[];
|
||||||
@@ -2809,7 +2810,7 @@ export function PipelinePage(props: {
|
|||||||
<section className="pipeline-page">
|
<section className="pipeline-page">
|
||||||
<header className="pl-head">
|
<header className="pl-head">
|
||||||
<div className="pl-title">
|
<div className="pl-title">
|
||||||
<button className="pl-back" type="button" onClick={() => navigate("projects")} aria-label="返回视频创作">
|
<button className="pl-back" type="button" onClick={() => (props.onBack ? props.onBack() : navigate("projects"))} aria-label="返回">
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`products-page${editMode ? " manage-mode" : ""}`}>
|
<section className={`products-page${editMode ? " manage-mode" : ""}`}>
|
||||||
|
<div className="pl-inner">
|
||||||
<header className="pl-head">
|
<header className="pl-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>商品库</h1>
|
<h1>商品库</h1>
|
||||||
@@ -319,6 +320,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat
|
|||||||
<Pager page={curPage} total={filtered.length} pageSize={PROD_PAGE_SIZE} onChange={setPage} />
|
<Pager page={curPage} total={filtered.length} pageSize={PROD_PAGE_SIZE} onChange={setPage} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`pl-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
<div className={`pl-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||||
<div className="pl-sel-copy">
|
<div className="pl-sel-copy">
|
||||||
@@ -473,6 +475,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="product-create-page">
|
<section className="product-create-page">
|
||||||
|
<div className="pc-inner">
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>新建商品</h1>
|
<h1>新建商品</h1>
|
||||||
@@ -587,6 +590,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -952,6 +956,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="product-detail-page">
|
<section className="product-detail-page">
|
||||||
|
<div className="pd-inner">
|
||||||
|
|
||||||
{/* 顶部 标题 + 状态 */}
|
{/* 顶部 标题 + 状态 */}
|
||||||
<div className="pd-title">
|
<div className="pd-title">
|
||||||
@@ -1336,6 +1341,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MediaLightbox open={!!preview} src={preview?.src || ""} kind="image" name={preview?.name} close={() => setPreview(null)} />
|
<MediaLightbox open={!!preview} src={preview?.src || ""} kind="image" name={preview?.name} close={() => setPreview(null)} />
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
|
|||||||
<section className="project-wizard-page">
|
<section className="project-wizard-page">
|
||||||
<header className="nw-head">
|
<header className="nw-head">
|
||||||
<div className="nw-title">
|
<div className="nw-title">
|
||||||
<button className="nw-back" type="button" onClick={onBack} aria-label="返回视频创作">
|
<button className="nw-back" type="button" onClick={onBack} aria-label="返回">
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
@@ -566,6 +566,7 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`projects-page${editMode ? " manage-mode" : ""}`}>
|
<section className={`projects-page${editMode ? " manage-mode" : ""}`}>
|
||||||
|
<div className="projects-inner">
|
||||||
<header className="vc-head">
|
<header className="vc-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>视频创作</h1>
|
<h1>视频创作</h1>
|
||||||
@@ -750,6 +751,7 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
|
|||||||
<Pager page={curPage} total={filtered.length} pageSize={PROJ_PAGE_SIZE} onChange={setPage} />
|
<Pager page={curPage} total={filtered.length} pageSize={PROJ_PAGE_SIZE} onChange={setPage} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`vc-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
<div className={`vc-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||||
<div className="vc-sel-copy">
|
<div className="vc-sel-copy">
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function isPage(value: string): value is Page {
|
|||||||
|
|
||||||
export function parentPage(page: Page): Page {
|
export function parentPage(page: Page): Page {
|
||||||
if (["productDetail", "productCreateUpload"].includes(page)) return "products";
|
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)) {
|
if (["imageOptimize", "modelPhoto", "modelPhotoDemoA", "modelPhotoDemoB", "platformCover"].includes(page)) {
|
||||||
return "assetFactory";
|
return "assetFactory";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,6 +387,7 @@ export function SettingsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings-page">
|
<section className="settings-page">
|
||||||
|
<div className="settings-inner">
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>设置</h1>
|
<h1>设置</h1>
|
||||||
@@ -682,6 +683,7 @@ export function SettingsPage({
|
|||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 上传头像 modal · 选图 → FormData(file) → onUploadAvatar */}
|
{/* 上传头像 modal · 选图 → FormData(file) → onUploadAvatar */}
|
||||||
<TeamModal
|
<TeamModal
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="team-page">
|
<section className="team-page">
|
||||||
|
<div className="team-inner">
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>团队中心</h1>
|
<h1>团队中心</h1>
|
||||||
@@ -580,6 +581,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 设置月限额(团队级)· 预设 pill + 实时剩余 */}
|
{/* 设置月限额(团队级)· 预设 pill + 实时剩余 */}
|
||||||
<TeamModal
|
<TeamModal
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProduc
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="trash-page">
|
<div className="trash-page">
|
||||||
|
<div className="trash-inner">
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>垃圾桶</h1>
|
<h1>垃圾桶</h1>
|
||||||
@@ -371,6 +372,7 @@ export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProduc
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={purgeAllOpen}
|
open={purgeAllOpen}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
|
|
||||||
|
.settings-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.page-head { margin-bottom: 22px; }
|
.page-head { margin-bottom: 22px; }
|
||||||
.page-head h1 { font-size: 28px; }
|
.page-head h1 { font-size: 28px; }
|
||||||
.page-head .sub { margin-top: 6px; font-size: 15px; color: var(--st-muted); }
|
.page-head .sub { margin-top: 6px; font-size: 15px; color: var(--st-muted); }
|
||||||
|
|||||||
@@ -624,7 +624,7 @@ table.t tbody tr:hover { background: var(--bg-soft); }
|
|||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
/* ─── React implementation: keeps the v1 visual language while binding real data ─── */
|
/* ─── React implementation: keeps the v1 visual language while binding real data ─── */
|
||||||
nav button {
|
.sidebar nav button {
|
||||||
display: flex; align-items: center; gap: 11px;
|
display: flex; align-items: center; gap: 11px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
color: var(--ink-2);
|
color: var(--ink-2);
|
||||||
@@ -635,9 +635,9 @@ nav button {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
nav button:hover { background: var(--bg-soft); color: var(--ink); }
|
.sidebar nav button:hover { background: var(--bg-soft); color: var(--ink); }
|
||||||
nav button.active { background: var(--orange-tint); color: var(--orange); }
|
.sidebar nav button.active { background: var(--orange-tint); color: var(--orange); }
|
||||||
nav button svg { width: 14px; height: 14px; color: var(--ink-3); }
|
.sidebar nav button svg { width: 14px; height: 14px; color: var(--ink-3); }
|
||||||
.crumbs button { color: var(--ink-3); }
|
.crumbs button { color: var(--ink-3); }
|
||||||
.crumbs button:hover { color: var(--ink); }
|
.crumbs button:hover { color: var(--ink); }
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
margin: -24px -28px -60px;
|
margin: -24px -28px -60px;
|
||||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||||
|
|
||||||
|
.team-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.page-head { margin-bottom: 34px; }
|
.page-head { margin-bottom: 34px; }
|
||||||
.page-head h1 { font-size: 32px; line-height: 1.2; letter-spacing: -.02em; }
|
.page-head h1 { font-size: 32px; line-height: 1.2; letter-spacing: -.02em; }
|
||||||
.page-head .sub { margin-top: 10px; font-size: 15px; color: var(--st-muted); }
|
.page-head .sub { margin-top: 10px; font-size: 15px; color: var(--st-muted); }
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
/* 垃圾桶 · 已删除商品(软删 status=archived)+ 已删除资产(is_deleted,R108)· 仅用 design-restraint token */
|
/* 垃圾桶 · 已删除商品(软删 status=archived)+ 已删除资产(is_deleted,R108)· 仅用 design-restraint token */
|
||||||
|
.trash-page .trash-inner {
|
||||||
|
width: min(1560px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.trash-empty { min-height: 220px; flex-direction: column; gap: 10px; }
|
.trash-empty { min-height: 220px; flex-direction: column; gap: 10px; }
|
||||||
|
|
||||||
/* 分区(商品 / 资产):mono 小标题 + 各自列表 */
|
/* 分区(商品 / 资产):mono 小标题 + 各自列表 */
|
||||||
|
|||||||
Reference in New Issue
Block a user