优化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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`shell-account-menu${show ? " show" : ""}`}
|
||||
id="shell-account-menu"
|
||||
role="menu"
|
||||
aria-label="账户菜单"
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
<div className="shell-account-head">
|
||||
<span className="av">{user.avatar_url ? <img src={user.avatar_url} alt="头像" /> : avatar}</span>
|
||||
<span>
|
||||
<span className="nm">{team?.name || user.username}</span>
|
||||
<span className="mail">{user.username}</span>
|
||||
</span>
|
||||
</div>
|
||||
{ACCOUNT_ITEMS.filter((item) => canManageBilling || (item.act !== "team" && item.act !== "account")).map((item) => (
|
||||
<button key={item.act} type="button" role="menuitem" onClick={() => go(item.act)}>
|
||||
<IconKitSvg name={item.icon} />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="sep" />
|
||||
<button type="button" role="menuitem" onClick={() => { onClose(); logout(); }}>
|
||||
<LogOut size={14} />
|
||||
退出登录
|
||||
</button>
|
||||
</div>,
|
||||
document.body
|
||||
return (
|
||||
<>
|
||||
{mounted && rect ? createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`shell-account-menu${show ? " show" : ""}`}
|
||||
id="shell-account-menu"
|
||||
role="menu"
|
||||
aria-label="账户菜单"
|
||||
style={{ left: pos.left, top: pos.top }}
|
||||
>
|
||||
<div className="shell-account-head">
|
||||
<span className="av">{user.avatar_url ? <img src={user.avatar_url} alt="头像" /> : avatar}</span>
|
||||
<span>
|
||||
<span className="nm">{team?.name || user.username}</span>
|
||||
<span className="mail">{user.username}</span>
|
||||
</span>
|
||||
</div>
|
||||
{ACCOUNT_ITEMS.filter((item) => canManageBilling || (item.act !== "team" && item.act !== "account")).map((item) => (
|
||||
<button key={item.act} type="button" role="menuitem" onClick={() => go(item.act)}>
|
||||
<IconKitSvg name={item.icon} />
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="sep" />
|
||||
<button type="button" role="menuitem" onClick={() => { onClose(); setConfirmLogout(true); }}>
|
||||
<LogOut size={14} />
|
||||
退出登录
|
||||
</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" },
|
||||
];
|
||||
|
||||
// 顶栏三按钮滑动黑胶囊 · 对照影擎 .mode-tabs / moveModeIndicator 原样接入:
|
||||
// clip-path 只露出三个按钮形状,胶囊在缝里看不见;位移走 transform,不在 React state 里改尺寸。
|
||||
// 顶栏三按钮滑动黑胶囊 · 结构 / 类名 / moveModeIndicator 均按影擎 HTML 原样转写。
|
||||
export function ModeTabs({ active, navigate }: { active: TopModule | null; navigate: Navigate }) {
|
||||
const btnRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const clipRectsRef = useRef<Array<SVGRectElement | null>>([]);
|
||||
const rootRef = useRef<HTMLElement>(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) => {
|
||||
btnRefs.current.forEach((item, index) => {
|
||||
const rect = clipRectsRef.current[index];
|
||||
if (!item || !rect) return;
|
||||
const root = rootRef.current;
|
||||
const modeIndicator = indicatorRef.current;
|
||||
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("y", "0");
|
||||
rect.setAttribute("width", String(item.offsetWidth));
|
||||
rect.setAttribute("height", String(item.offsetHeight));
|
||||
});
|
||||
const modeIndicator = indicatorRef.current;
|
||||
if (!modeIndicator) return;
|
||||
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.transform = `translateX(${tab.offsetLeft}px)`;
|
||||
if (!animate) {
|
||||
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 idx = active ? MODE_TABS.findIndex((tab) => tab.id === active) : -1;
|
||||
return idx >= 0 ? btnRefs.current[idx] : null;
|
||||
const tabFor = (id: TopModule | null) => {
|
||||
const root = rootRef.current;
|
||||
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(() => {
|
||||
const tab = activeButton();
|
||||
const tab = tabFor(active);
|
||||
if (tab) moveModeIndicator(tab, !firstPaint.current);
|
||||
firstPaint.current = false;
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
const snap = () => {
|
||||
const tab = activeButton();
|
||||
const tab = tabFor(activeRef.current);
|
||||
if (tab) moveModeIndicator(tab, false);
|
||||
};
|
||||
window.addEventListener("resize", snap);
|
||||
void document.fonts?.ready.then(snap);
|
||||
return () => window.removeEventListener("resize", snap);
|
||||
}, [active]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`yz-mods${active ? "" : " no-active"}`}
|
||||
role="tablist"
|
||||
aria-label="创作模块"
|
||||
<nav
|
||||
ref={rootRef}
|
||||
className={`mode-tabs${visualActive || active ? "" : " no-active"}`}
|
||||
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>
|
||||
<clipPath id="yzModeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||
<rect ref={(el) => { clipRectsRef.current[0] = el; }} 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 ref={(el) => { clipRectsRef.current[2] = el; }} rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||
<clipPath id="modeButtonMask" clipPathUnits="userSpaceOnUse">
|
||||
<rect rx="22" ry="22" x="0" y="0" width="104" height="44" />
|
||||
<rect rx="22" ry="22" x="120" y="0" width="110" height="44" />
|
||||
<rect rx="22" ry="22" x="246" y="0" width="110" height="44" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="yz-mod-indicator-mask" aria-hidden="true">
|
||||
<span
|
||||
ref={indicatorRef}
|
||||
className="yz-mod-indicator"
|
||||
style={{ width: 104, transform: "translateX(0px)" }}
|
||||
/>
|
||||
<span className="mode-indicator-mask" aria-hidden="true">
|
||||
<span ref={indicatorRef} className="mode-indicator" id="modeIndicator" />
|
||||
</span>
|
||||
{MODE_TABS.map((tab, index) => (
|
||||
{MODE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
ref={(el) => { btnRefs.current[index] = el; }}
|
||||
className={`yz-mod${active === tab.id ? " active" : ""}`}
|
||||
className={`mode-tab${(visualActive ?? active) === tab.id ? " active" : ""}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active === tab.id}
|
||||
onClick={() => {
|
||||
const btn = btnRefs.current[index];
|
||||
if (btn) moveModeIndicator(btn, true);
|
||||
data-page={tab.id}
|
||||
onClick={(event) => {
|
||||
setVisualActive(tab.id);
|
||||
moveModeIndicator(event.currentTarget, true);
|
||||
navigate(tab.page);
|
||||
}}
|
||||
>{tab.label}</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,17 @@
|
||||
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;
|
||||
}
|
||||
@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 {
|
||||
@@ -37,7 +42,6 @@
|
||||
.dashboard-page .welcome,
|
||||
.dashboard-page .creation {
|
||||
width: min(1140px, 100%);
|
||||
margin-right: auto;
|
||||
}
|
||||
.dashboard-page .creation { margin-top: 40px; }
|
||||
.dashboard-page .creation-head {
|
||||
|
||||
@@ -779,7 +779,8 @@ main { position: relative; background: #fff; min-width: 0; }
|
||||
.crumbs .sep { color: var(--black-alpha-32); }
|
||||
.crumbs .here { color: var(--accent-black); font-weight: 500; }
|
||||
.crumbs a:hover { color: var(--accent-black); }
|
||||
.topbar .yz-mods {
|
||||
/* 顶栏工作模式 · 影擎 .mode-tabs / .mode-tab / moveModeIndicator 原样 */
|
||||
.topbar .mode-tabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -789,25 +790,23 @@ main { position: relative; background: #fff; min-width: 0; }
|
||||
width: max-content;
|
||||
max-width: none;
|
||||
gap: 16px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.topbar .yz-mods.no-active .yz-mod-indicator-mask { opacity: 0; }
|
||||
.yz-mod-clip-defs {
|
||||
.topbar .mode-tabs.no-active .mode-indicator-mask { opacity: 0; }
|
||||
.mode-clip-defs {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
max-width: none;
|
||||
}
|
||||
.yz-mod-indicator-mask {
|
||||
.mode-indicator-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
clip-path: url(#yzModeButtonMask);
|
||||
-webkit-clip-path: url(#yzModeButtonMask);
|
||||
transition: opacity 180ms ease;
|
||||
}
|
||||
.yz-mod-indicator {
|
||||
.mode-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -817,43 +816,38 @@ main { position: relative; background: #fff; min-width: 0; }
|
||||
background: #101012;
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.16);
|
||||
pointer-events: none;
|
||||
transform: translateX(0px);
|
||||
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);
|
||||
will-change: transform, width;
|
||||
}
|
||||
.topbar .yz-mods .yz-mod {
|
||||
.topbar .mode-tabs .mode-tab {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
min-width: 104px;
|
||||
height: 44px;
|
||||
padding: 0 22px;
|
||||
gap: 0;
|
||||
border: 1px solid #d9dde3;
|
||||
border-radius: 22px;
|
||||
color: #2b2e33;
|
||||
background: rgba(31, 39, 51, 0.025);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
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 .yz-mods .yz-mod.active {
|
||||
.topbar .mode-tabs .mode-tab.active {
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -2883,8 +2877,6 @@ table.t tbody tr:hover { background: var(--black-alpha-4); }
|
||||
.dash-funds-mono:hover { color: var(--heat); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.yz-mod-indicator,
|
||||
.yz-mod-indicator-mask,
|
||||
.modal-bg,
|
||||
.modal,
|
||||
.drawer,
|
||||
|
||||
@@ -30,6 +30,15 @@
|
||||
.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 {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
.library-page { margin: -28px -24px -48px; }
|
||||
}
|
||||
|
||||
.library-page .lib-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.library-page .lib-head {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
@@ -410,8 +415,10 @@
|
||||
.library-page .lib-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.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)); }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.library-page .lib-sel-bar { left: 50%; }
|
||||
.library-page .lib-toolbar { flex-wrap: wrap; }
|
||||
}
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
.models-page { margin: -28px -24px -48px; }
|
||||
}
|
||||
|
||||
.models-page .ml-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.models-page .ml-head {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
@@ -105,8 +110,8 @@
|
||||
|
||||
.models-page .ml-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
.models-page .ml-card {
|
||||
position: relative;
|
||||
@@ -254,11 +259,10 @@
|
||||
.models-page .ml-sel-confirm { color: #fff; background: #002fa7; }
|
||||
.models-page .ml-sel-confirm:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
@media (max-width: 1500px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.models-page .ml-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.models-page .ml-sel-bar { left: 50%; }
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
全部规则 scope 在 .product-create-page 下,避免污染他页。
|
||||
============================================================ */
|
||||
|
||||
.product-create-page .pc-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ─── 主表单双栏 ─── */
|
||||
.product-create-page .form-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
}
|
||||
|
||||
.product-detail-page {
|
||||
.pd-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ─── 顶部 标题 + 状态 ─── */
|
||||
.pd-title {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
margin: -24px -28px -60px;
|
||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||
|
||||
.pl-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pl-head {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
@@ -447,9 +452,11 @@
|
||||
.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) {
|
||||
.products-page { margin: -28px -24px -48px; }
|
||||
.products-page .pl-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.products-page .pl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
.projects-page { margin: -28px -24px -48px; }
|
||||
}
|
||||
|
||||
.projects-page .projects-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.projects-page .vc-head {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
@@ -288,7 +293,8 @@
|
||||
|
||||
.projects-page .vc-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;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@@ -504,18 +510,13 @@
|
||||
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) {
|
||||
.projects-page .vc-tools.two,
|
||||
.projects-page .vc-tools.three { grid-template-columns: 1fr; }
|
||||
.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%; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.projects-page .vc-grid:not(.list) { grid-template-columns: 1fr; }
|
||||
.projects-page .vc-search { width: 100%; }
|
||||
}
|
||||
|
||||
|
||||
@@ -287,6 +287,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
return (
|
||||
<section className="account-page">
|
||||
<div className="ac-inner">
|
||||
<header className="ac-head">
|
||||
<div>
|
||||
<h1>账单库</h1>
|
||||
@@ -617,6 +618,7 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TopupModal
|
||||
open={topupChannel !== null}
|
||||
|
||||
@@ -265,6 +265,7 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
|
||||
|
||||
return (
|
||||
<div className="asset-factory">
|
||||
<div className="asset-factory-inner">
|
||||
<header className="af-page-head">
|
||||
<div>
|
||||
<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} />
|
||||
</div>
|
||||
|
||||
{/* 批次详情弹窗:展示该批生成的全部图片(参考资产库),点图放大 */}
|
||||
{openBatch && createPortal(
|
||||
@@ -1600,7 +1602,7 @@ export function ImageWorkbenchPage({
|
||||
<div className="yz-image image-workbench">
|
||||
<header className="image-subpage-header">
|
||||
<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 />
|
||||
</button>
|
||||
<div>
|
||||
@@ -1811,7 +1813,7 @@ export function ImageWorkbenchPage({
|
||||
<div className="yz-image image-workbench">
|
||||
<header className="image-subpage-header">
|
||||
<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 />
|
||||
</button>
|
||||
<div>
|
||||
|
||||
@@ -128,6 +128,7 @@ export function Dashboard({
|
||||
|
||||
return (
|
||||
<section className="dashboard-page">
|
||||
<div className="dashboard-inner">
|
||||
<section className="welcome">
|
||||
<h1>欢迎回来{greetName ? `,${greetName}` : ""}</h1>
|
||||
<p>{dateLabel} · 你有{running}个项目正在进行中</p>
|
||||
@@ -258,6 +259,7 @@ export function Dashboard({
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -529,6 +529,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
|
||||
|
||||
return (
|
||||
<div className="fc-page">
|
||||
<div className="fc-inner">
|
||||
<header className="fc-head">
|
||||
<div className="fc-title-row">
|
||||
{onBack ? (
|
||||
@@ -606,6 +607,7 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
|
||||
onClear={clearInput}
|
||||
onSend={() => void handleSend()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{detailTask && (
|
||||
<VideoDetailModal
|
||||
|
||||
@@ -661,6 +661,7 @@ export function LibraryPage({ onUpload, onDeleteMany }: { onUpload: (formData: F
|
||||
|
||||
return (
|
||||
<section className={`library-page${editMode ? " manage-mode" : ""}`}>
|
||||
<div className="lib-inner">
|
||||
<header className="lib-head">
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<div className={`lib-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||
<div className="lib-sel-copy">
|
||||
|
||||
@@ -363,6 +363,7 @@ export function MessagesPage({ unreadCount, onMarkRead, onMarkAllRead, onArchive
|
||||
|
||||
return (
|
||||
<div className="msg-page">
|
||||
<div className="msg-inner">
|
||||
<div className="page-head">
|
||||
<button className="msg-back" type="button" onClick={() => navigate("dashboard")} aria-label="返回">
|
||||
<ArrowLeft size={18} strokeWidth={1.75} />
|
||||
@@ -537,6 +538,7 @@ export function MessagesPage({ unreadCount, onMarkRead, onMarkAllRead, onArchive
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toast && (
|
||||
<div className="toast show" role="status" aria-live="polite">
|
||||
|
||||
@@ -344,6 +344,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
|
||||
|
||||
return (
|
||||
<section className={`models-page${editMode ? " manage-mode" : ""}`}>
|
||||
<div className="ml-inner">
|
||||
<header className="ml-head">
|
||||
<div>
|
||||
<h1>模特库</h1>
|
||||
@@ -448,6 +449,7 @@ export function ModelsPage({ onNotify, onBillingChanged }: {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`ml-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||
<div className="ml-sel-copy">
|
||||
|
||||
@@ -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: {
|
||||
<section className="pipeline-page">
|
||||
<header className="pl-head">
|
||||
<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 />
|
||||
</button>
|
||||
<div>
|
||||
|
||||
@@ -207,6 +207,7 @@ export function ProductsPage({ products, projects = [], loading = false, navigat
|
||||
|
||||
return (
|
||||
<section className={`products-page${editMode ? " manage-mode" : ""}`}>
|
||||
<div className="pl-inner">
|
||||
<header className="pl-head">
|
||||
<div>
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`pl-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||
<div className="pl-sel-copy">
|
||||
@@ -473,6 +475,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
|
||||
|
||||
return (
|
||||
<section className="product-create-page">
|
||||
<div className="pc-inner">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>新建商品</h1>
|
||||
@@ -587,6 +590,7 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -952,6 +956,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
|
||||
return (
|
||||
<section className="product-detail-page">
|
||||
<div className="pd-inner">
|
||||
|
||||
{/* 顶部 标题 + 状态 */}
|
||||
<div className="pd-title">
|
||||
@@ -1336,6 +1341,7 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<header className="nw-head">
|
||||
<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 />
|
||||
</button>
|
||||
<div>
|
||||
@@ -566,6 +566,7 @@ export function ProjectsPage({ products, projects, loading = false, navigate, op
|
||||
|
||||
return (
|
||||
<section className={`projects-page${editMode ? " manage-mode" : ""}`}>
|
||||
<div className="projects-inner">
|
||||
<header className="vc-head">
|
||||
<div>
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`vc-sel-bar${editMode ? " active" : ""}`} role="toolbar" aria-label="批量操作">
|
||||
<div className="vc-sel-copy">
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -387,6 +387,7 @@ export function SettingsPage({
|
||||
|
||||
return (
|
||||
<section className="settings-page">
|
||||
<div className="settings-inner">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>设置</h1>
|
||||
@@ -682,6 +683,7 @@ export function SettingsPage({
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上传头像 modal · 选图 → FormData(file) → onUploadAvatar */}
|
||||
<TeamModal
|
||||
|
||||
@@ -345,6 +345,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
|
||||
|
||||
return (
|
||||
<section className="team-page">
|
||||
<div className="team-inner">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>团队中心</h1>
|
||||
@@ -580,6 +581,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* 设置月限额(团队级)· 预设 pill + 实时剩余 */}
|
||||
<TeamModal
|
||||
|
||||
@@ -309,6 +309,7 @@ export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProduc
|
||||
|
||||
return (
|
||||
<div className="trash-page">
|
||||
<div className="trash-inner">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>垃圾桶</h1>
|
||||
@@ -371,6 +372,7 @@ export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProduc
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
open={purgeAllOpen}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
margin: -24px -28px -60px;
|
||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||
|
||||
.settings-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-head { margin-bottom: 22px; }
|
||||
.page-head h1 { font-size: 28px; }
|
||||
.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); } }
|
||||
|
||||
/* ─── React implementation: keeps the v1 visual language while binding real data ─── */
|
||||
nav button {
|
||||
.sidebar nav button {
|
||||
display: flex; align-items: center; gap: 11px;
|
||||
padding: 8px 12px;
|
||||
color: var(--ink-2);
|
||||
@@ -635,9 +635,9 @@ nav button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
nav button:hover { background: var(--bg-soft); color: var(--ink); }
|
||||
nav button.active { background: var(--orange-tint); color: var(--orange); }
|
||||
nav button svg { width: 14px; height: 14px; color: var(--ink-3); }
|
||||
.sidebar nav button:hover { background: var(--bg-soft); color: var(--ink); }
|
||||
.sidebar nav button.active { background: var(--orange-tint); color: var(--orange); }
|
||||
.sidebar nav button svg { width: 14px; height: 14px; color: var(--ink-3); }
|
||||
.crumbs button { color: var(--ink-3); }
|
||||
.crumbs button:hover { color: var(--ink); }
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
margin: -24px -28px -60px;
|
||||
padding: 52px clamp(40px, 3.2vw, 68px) 48px;
|
||||
|
||||
.team-inner {
|
||||
width: min(1560px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-head { margin-bottom: 34px; }
|
||||
.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); }
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* 垃圾桶 · 已删除商品(软删 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; }
|
||||
|
||||
/* 分区(商品 / 资产):mono 小标题 + 各自列表 */
|
||||
|
||||
Reference in New Issue
Block a user