feat(core/frontend): pipeline stage editor (burn-in controls) + double-submit guard & button greying
Pipeline (脚本→资产→故事板→视频→拼接): - Stage1 render real script shots + wire 确认脚本→adopt (advance stage) - Stage2 add person/scene AI-生成 buttons + clickable category tabs - Stage4 auto-poll videos to completion + per-segment upload + real frame thumbnails + download - Stage5 real timeline editor: clips undo/redo/split/copy/delete/drag-reorder/zoom, subtitle style + per-clip text editor, transition select (xfade preview), BGM upload + volume, save draft, export-with-save → shows/download final MP4 - embedded asset URLs everywhere (beat assets pagination) UX: re-entry guard in action() (no double-submit anywhere) + greyed :disabled styles for btn-aigen/chat-mode/pill-cta/tl-action so generate buttons visibly disable while generating. Also includes prior uncommitted frontend work: settings preferences/sessions/avatar, asset delete, account/team/products pages, fonts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ChangeEvent } from "react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -52,6 +53,24 @@ function statusText(status: string) {
|
||||
return STATUS_LABEL[status] || status;
|
||||
}
|
||||
|
||||
// 下载图片:优先 fetch→blob 触发真实下载;跨域失败则回退到新标签打开(用户仍拿到图)
|
||||
async function downloadImage(url: string, filename: string) {
|
||||
try {
|
||||
const res = await fetch(url, { mode: "cors" });
|
||||
const blob = await res.blob();
|
||||
const href = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(href);
|
||||
} catch {
|
||||
window.open(url, "_blank", "noopener");
|
||||
}
|
||||
}
|
||||
|
||||
export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page) => void; aiTasks: AITask[] }) {
|
||||
const cards = [
|
||||
{
|
||||
@@ -88,7 +107,40 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
|
||||
return acc;
|
||||
}, [aiTasks]);
|
||||
|
||||
const visible = aiTasks.slice(0, 8);
|
||||
// 任务中心筛选:状态 tab / 搜索 / 时间 / 任务类型 / 网格·列表视图 —— 全部对真实 aiTasks 生效
|
||||
const [filter, setFilter] = useState<"all" | "gen" | "ok" | "err">("all");
|
||||
const [query, setQuery] = useState("");
|
||||
const [timeFilter, setTimeFilter] = useState<"all" | "1" | "7" | "30">("all");
|
||||
const [typeFilter, setTypeFilter] = useState("");
|
||||
const [view, setView] = useState<"grid" | "list">("list");
|
||||
const [openChip, setOpenChip] = useState<"" | "time" | "type">("");
|
||||
useEffect(() => {
|
||||
if (!openChip) return;
|
||||
const close = (event: MouseEvent) => { if (!(event.target as HTMLElement).closest(".chip-wrap")) setOpenChip(""); };
|
||||
document.addEventListener("click", close);
|
||||
return () => document.removeEventListener("click", close);
|
||||
}, [openChip]);
|
||||
|
||||
const typeOptions = Array.from(new Set(aiTasks.map((t) => t.task_type).filter(Boolean)));
|
||||
const TIME_OPTS: Array<{ value: typeof timeFilter; label: string }> = [
|
||||
{ value: "all", label: "全部时间" }, { value: "1", label: "今天" }, { value: "7", label: "近 7 天" }, { value: "30", label: "近 30 天" }
|
||||
];
|
||||
const visible = aiTasks.filter((task) => {
|
||||
const pill = statusPill(task.status);
|
||||
if (filter === "gen" && pill !== "info") return false;
|
||||
if (filter === "ok" && pill !== "ok") return false;
|
||||
if (filter === "err" && pill !== "err") return false;
|
||||
if (typeFilter && task.task_type !== typeFilter) return false;
|
||||
if (timeFilter !== "all" && task.created_at) {
|
||||
const days = (Date.now() - new Date(task.created_at).getTime()) / 86400000;
|
||||
if (days > Number(timeFilter)) return false;
|
||||
}
|
||||
if (query) {
|
||||
const hay = `${TASK_TYPE_LABEL[task.task_type] || task.task_type} ${task.task_type} ${task.id}`.toLowerCase();
|
||||
if (!hay.includes(query.toLowerCase())) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="asset-factory">
|
||||
@@ -133,21 +185,63 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 状态 tabs(转写自 asset-factory.html #tc-tabs) */}
|
||||
<div className="tabs" id="tc-tabs">
|
||||
<div className={`tab${filter === "all" ? " active" : ""}`} data-filter="all" role="button" tabIndex={0} onClick={() => setFilter("all")}>全部 <span className="count">{aiTasks.length}</span></div>
|
||||
<div className={`tab${filter === "gen" ? " active" : ""}`} data-filter="gen" role="button" tabIndex={0} onClick={() => setFilter("gen")}>生成中 <span className="count">{counts.gen}</span></div>
|
||||
<div className={`tab${filter === "ok" ? " active" : ""}`} data-filter="ok" role="button" tabIndex={0} onClick={() => setFilter("ok")}>已完成 <span className="count">{counts.ok}</span></div>
|
||||
<div className={`tab${filter === "err" ? " active" : ""}`} data-filter="err" role="button" tabIndex={0} onClick={() => setFilter("err")}>失败 <span className="count">{counts.err}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<div className="search-inline">
|
||||
<Search size={14} />
|
||||
<input className="input" placeholder="搜索任务名" />
|
||||
<input className="input" placeholder="搜索任务名" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
</div>
|
||||
<div className={`chip-wrap${openChip === "time" ? " open" : ""}`} data-key="time">
|
||||
<button className={`chip${timeFilter !== "all" ? " active" : ""}`} type="button" onClick={() => setOpenChip((c) => (c === "time" ? "" : "time"))}>
|
||||
<span className="chip-label">{TIME_OPTS.find((o) => o.value === timeFilter)?.label !== "全部时间" ? TIME_OPTS.find((o) => o.value === timeFilter)?.label : "时间"}</span> <svg className="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg>
|
||||
</button>
|
||||
<div className="chip-menu">
|
||||
{TIME_OPTS.map((opt) => (
|
||||
<div className={`mi${timeFilter === opt.value ? " selected" : ""}`} key={opt.value} role="button" tabIndex={0} onClick={() => { setTimeFilter(opt.value); setOpenChip(""); }}>
|
||||
<svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>{opt.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`chip-wrap${openChip === "type" ? " open" : ""}`} data-key="type">
|
||||
<button className={`chip${typeFilter ? " active" : ""}`} type="button" onClick={() => setOpenChip((c) => (c === "type" ? "" : "type"))}>
|
||||
<span className="chip-label">{typeFilter ? TASK_TYPE_LABEL[typeFilter] || typeFilter : "任务类型"}</span> <svg className="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg>
|
||||
</button>
|
||||
<div className="chip-menu">
|
||||
<div className={`mi${!typeFilter ? " selected" : ""}`} role="button" tabIndex={0} onClick={() => { setTypeFilter(""); setOpenChip(""); }}>
|
||||
<svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>全部类型
|
||||
</div>
|
||||
{typeOptions.length > 0 && <div className="mi-sep" />}
|
||||
{typeOptions.map((t) => (
|
||||
<div className={`mi${typeFilter === t ? " selected" : ""}`} key={t} role="button" tabIndex={0} onClick={() => { setTypeFilter(t); setOpenChip(""); }}>
|
||||
<svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>{TASK_TYPE_LABEL[t] || t}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{(filter !== "all" || query || timeFilter !== "all" || typeFilter) && (
|
||||
<button className="clear-filters" type="button" onClick={() => { setFilter("all"); setQuery(""); setTimeFilter("all"); setTypeFilter(""); }}>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 4l8 8M12 4l-8 8" /></svg>
|
||||
清空筛选
|
||||
</button>
|
||||
)}
|
||||
<span className="spacer" />
|
||||
<div className="view-toggle">
|
||||
<button type="button" className="active">
|
||||
<List size={13} />
|
||||
列表
|
||||
</button>
|
||||
<button type="button">
|
||||
<button type="button" className={view === "grid" ? "active" : ""} data-view="grid" onClick={() => setView("grid")}>
|
||||
<Grid2X2 size={13} />
|
||||
网格
|
||||
</button>
|
||||
<button type="button" className={view === "list" ? "active" : ""} data-view="list" onClick={() => setView("list")}>
|
||||
<List size={13} />
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +254,31 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
|
||||
<div className="mono">// NO TASKS YET</div>
|
||||
<div>还没有任务,去上方选一个工序开始生成吧</div>
|
||||
</div>
|
||||
) : visible.length === 0 ? (
|
||||
<div className="task-empty">
|
||||
<div className="mono">// NO MATCH</div>
|
||||
<div>没有符合筛选条件的任务</div>
|
||||
</div>
|
||||
) : view === "grid" ? (
|
||||
<div className="history-grid">
|
||||
{visible.map((task) => {
|
||||
const pill = statusPill(task.status);
|
||||
const typeLabel = TASK_TYPE_LABEL[task.task_type] || task.task_type;
|
||||
return (
|
||||
<article className="task-card history-card" key={task.id}>
|
||||
<div className="placeholder"><span className="ph-frame">{task.id.slice(0, 4)}</span></div>
|
||||
<div className="history-body">
|
||||
<div className="history-name">{typeLabel}</div>
|
||||
<div className="history-type">// {task.task_type}</div>
|
||||
<div className="history-foot">
|
||||
<span className="mono">{(task.created_at || "").slice(0, 10)}</span>
|
||||
<span className={`pill ${pill}`}><span className="dot" />{statusText(task.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="task-list-view">
|
||||
<table className="t">
|
||||
@@ -168,7 +287,8 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
|
||||
<th style={{ width: "42%" }}>任务</th>
|
||||
<th style={{ width: 160 }}>进度</th>
|
||||
<th>状态</th>
|
||||
<th style={{ width: 140 }}>任务 ID</th>
|
||||
<th style={{ width: 120 }}>创建于</th>
|
||||
<th style={{ width: 48 }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -208,7 +328,8 @@ export function AssetFactoryPage({ navigate, aiTasks }: { navigate: (page: Page)
|
||||
{statusText(task.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="muted-2">{task.id.slice(0, 8)}</td>
|
||||
<td className="muted-2 mono" style={{ fontSize: 11 }}>{(task.created_at || "").slice(0, 10)}</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
@@ -243,7 +364,7 @@ const MODE_META: Record<
|
||||
title: "模特上身图",
|
||||
tag: "[ MODEL · TRY-ON ]",
|
||||
desc: "选择模特和商品,生成电商模特上身图。",
|
||||
ratio: "3:4",
|
||||
ratio: "1:1",
|
||||
promptTemplate: (title) => `${title},模特上身展示,自然光,真实质感,电商主图`
|
||||
},
|
||||
cover: {
|
||||
@@ -328,6 +449,19 @@ export function ImageWorkbenchPage({
|
||||
const [pickedIds, setPickedIds] = useState<string[]>([]);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [results, setResults] = useState<Asset[] | null>(null);
|
||||
const [refImage, setRefImage] = useState<{ name: string; url: string } | null>(null);
|
||||
const refInputRef = useRef<HTMLInputElement | null>(null);
|
||||
// 模特/平台 工作台头部:搜索 + 时间排序 + 模特筛选(对左侧网格真实生效)
|
||||
const [gridQuery, setGridQuery] = useState("");
|
||||
const [gridSort, setGridSort] = useState<"recent" | "name">("recent");
|
||||
const [tbOpen, setTbOpen] = useState<"" | "time" | "model">("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
function pickReference(event: ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
setRefImage({ name: file.name, url: URL.createObjectURL(file) });
|
||||
event.target.value = "";
|
||||
}
|
||||
|
||||
const imageModels = modelConfigs.filter((model) => model.capability.includes("image"));
|
||||
|
||||
@@ -387,15 +521,19 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
)}
|
||||
<div className="gen-image-actions">
|
||||
<button className="gen-img-btn" type="button" title="重跑">
|
||||
<button className="gen-img-btn" type="button" title="重跑" disabled={generating} onClick={() => runGenerate()}>
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
<button className="gen-img-btn" type="button" title="下载">
|
||||
<Download size={14} />
|
||||
</button>
|
||||
<button className="gen-img-btn" type="button" title="更多">
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
{url && (
|
||||
<button className="gen-img-btn" type="button" title="下载" onClick={() => downloadImage(url, `${meta.title}-${index + 1}.png`)}>
|
||||
<Download size={14} />
|
||||
</button>
|
||||
)}
|
||||
{url && (
|
||||
<button className="gen-img-btn" type="button" title="查看原图" onClick={() => window.open(url, "_blank", "noopener")}>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -418,7 +556,7 @@ export function ImageWorkbenchPage({
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
<button className="ic-new-conv" type="button">
|
||||
<button className="ic-new-conv" type="button" onClick={() => { setResults(null); setPrompt(meta.promptTemplate(product?.title || "商品")); setPickedIds([]); }}>
|
||||
<Plus size={13} />
|
||||
新对话
|
||||
</button>
|
||||
@@ -464,15 +602,15 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
<div className="gen-card">{renderResultGrid()}</div>
|
||||
<div className="gen-card-actions">
|
||||
<button className="btn btn-sm" type="button">
|
||||
<button className="btn btn-sm" type="button" onClick={() => setResults(null)}>
|
||||
<RefreshCw size={13} />
|
||||
重新编辑
|
||||
</button>
|
||||
<button className="btn btn-sm" type="button">
|
||||
<button className="btn btn-sm" type="button" onClick={() => navigate?.("library")}>
|
||||
<Check size={13} />
|
||||
全部加入资产库
|
||||
</button>
|
||||
<button className="btn btn-sm btn-ghost" type="button" title="更多">
|
||||
<button className="btn btn-sm btn-ghost" type="button" title="下载全部" onClick={() => results?.forEach((asset, i) => { const u = asset.files?.[0]?.preview_url; if (u) downloadImage(u, `${meta.title}-${i + 1}.png`); })}>
|
||||
<MoreHorizontal size={13} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -501,9 +639,17 @@ export function ImageWorkbenchPage({
|
||||
<div className="ic-input-wrap">
|
||||
<div className="ic-input">
|
||||
<div className="ic-input-top">
|
||||
<button className="add-btn" type="button" title="上传参考图">
|
||||
<button className="add-btn" type="button" title="上传参考图" onClick={() => refInputRef.current?.click()}>
|
||||
<Plus size={22} />
|
||||
</button>
|
||||
<input ref={refInputRef} type="file" accept="image/*" hidden onChange={pickReference} />
|
||||
{refImage && (
|
||||
<span className="meta-chip" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
<img src={refImage.url} alt="参考图" style={{ width: 18, height: 18, borderRadius: 3, objectFit: "cover" }} />
|
||||
{refImage.name.slice(0, 16)}
|
||||
<button type="button" aria-label="移除参考图" style={{ border: 0, background: "none", cursor: "pointer", padding: 0, lineHeight: 1 }} onClick={() => setRefImage(null)}>×</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
className="ic-input-text"
|
||||
@@ -568,6 +714,12 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
<div className="iw-list-h">
|
||||
<span className="mono">// 商品空间</span>
|
||||
{navigate && (
|
||||
<button className="new-prod" type="button" title="新建商品" onClick={() => navigate("productCreateUpload")}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
|
||||
<span>新建商品</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="iw-ps-list">
|
||||
{products.length === 0 ? (
|
||||
@@ -607,13 +759,33 @@ export function ImageWorkbenchPage({
|
||||
</span>
|
||||
</div>
|
||||
<span className="spacer" />
|
||||
<button className="search-btn" type="button" title="搜索">
|
||||
<Search size={14} />
|
||||
</button>
|
||||
{mode === "model" && navigate && (
|
||||
<button className="tb-chip" type="button" onClick={() => navigate("modelPhotoDemoA")}>
|
||||
方案 A
|
||||
<div className="tb-search-wrap">
|
||||
{searchOpen && (
|
||||
<input className="input" autoFocus placeholder={mode === "model" ? "搜索模特" : "搜索平台"} value={gridQuery} onChange={(event) => setGridQuery(event.target.value)} style={{ height: 30, marginRight: 6, width: 140 }} />
|
||||
)}
|
||||
<button className={`search-btn${searchOpen ? " active" : ""}`} type="button" title={mode === "model" ? "搜索批次/模特" : "搜索"} onClick={() => { setSearchOpen((v) => !v); if (searchOpen) setGridQuery(""); }}>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{mode === "model" && (
|
||||
<>
|
||||
<div className={`tb-menu-wrap chip-wrap${tbOpen === "time" ? " open" : ""}`} data-filter="time">
|
||||
<button className={`tb-chip${gridSort === "name" ? " active" : ""}`} type="button" onClick={() => setTbOpen((c) => (c === "time" ? "" : "time"))}><span className="lbl">{gridSort === "name" ? "按名称" : "时间"}</span> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg></button>
|
||||
<div className="chip-menu align-right">
|
||||
<div className={`mi${gridSort === "recent" ? " selected" : ""}`} role="button" tabIndex={0} onClick={() => { setGridSort("recent"); setTbOpen(""); }}><svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>最近添加</div>
|
||||
<div className={`mi${gridSort === "name" ? " selected" : ""}`} role="button" tabIndex={0} onClick={() => { setGridSort("name"); setTbOpen(""); }}><svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>按名称</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`tb-menu-wrap chip-wrap${tbOpen === "model" ? " open" : ""}`} data-filter="model">
|
||||
<button className={`tb-chip${gridQuery ? " active" : ""}`} type="button" onClick={() => setTbOpen((c) => (c === "model" ? "" : "model"))}><span className="lbl">{gridQuery || "模特"}</span> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 6l4 4 4-4" /></svg></button>
|
||||
<div className="chip-menu align-right">
|
||||
<div className={`mi${!gridQuery ? " selected" : ""}`} role="button" tabIndex={0} onClick={() => { setGridQuery(""); setTbOpen(""); }}><svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>全部模特</div>
|
||||
{(personAssets.length > 0 ? personAssets.map((p) => p.name) : FALLBACK_MODELS.map((m) => m.name)).map((nm) => (
|
||||
<div className={`mi${gridQuery === nm ? " selected" : ""}`} key={nm} role="button" tabIndex={0} onClick={() => { setGridQuery(nm); setTbOpen(""); }}><svg className="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>{nm}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -629,7 +801,11 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
<div className="model-grid">
|
||||
{personAssets.length > 0
|
||||
? personAssets.slice(0, 6).map((item) => {
|
||||
? personAssets
|
||||
.filter((item) => !gridQuery || item.name.toLowerCase().includes(gridQuery.toLowerCase()))
|
||||
.sort((a, b) => (gridSort === "name" ? a.name.localeCompare(b.name) : (b.created_at || "").localeCompare(a.created_at || "")))
|
||||
.slice(0, 6)
|
||||
.map((item) => {
|
||||
const url = item.files?.[0]?.preview_url;
|
||||
return (
|
||||
<button
|
||||
@@ -655,7 +831,10 @@ export function ImageWorkbenchPage({
|
||||
</button>
|
||||
);
|
||||
})
|
||||
: FALLBACK_MODELS.map((item) => (
|
||||
: FALLBACK_MODELS
|
||||
.filter((item) => !gridQuery || item.name.toLowerCase().includes(gridQuery.toLowerCase()))
|
||||
.sort((a, b) => (gridSort === "name" ? a.name.localeCompare(b.name) : 0))
|
||||
.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
@@ -681,7 +860,7 @@ export function ImageWorkbenchPage({
|
||||
<span className="title">选择平台</span>
|
||||
</div>
|
||||
<div className="platform-grid">
|
||||
{PLATFORM_OPTIONS.map((item) => (
|
||||
{PLATFORM_OPTIONS.filter((item) => !gridQuery || item.name.toLowerCase().includes(gridQuery.toLowerCase())).map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
@@ -867,7 +1046,11 @@ const DEMO_SIDE_PRODUCTS = [
|
||||
{ id: "d6", title: "小熊 4L 可视空气炸锅", category: "家居家电", batches: 0 }
|
||||
];
|
||||
|
||||
export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A" | "B"; products: Product[]; onBack: () => void }) {
|
||||
export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { variant: "A" | "B"; products: Product[]; onBack: () => void; navigate?: (page: Page) => void }) {
|
||||
// 方案 A/B 是设计展示页(mock 数据,无生成后端);所有动作按钮导向真实工具 / 改本地选中态,杜绝死按钮
|
||||
const [demoCount, setDemoCount] = useState("4 张");
|
||||
const [demoRatio, setDemoRatio] = useState("3:4");
|
||||
const toRealTool = () => onBack();
|
||||
// 左栏商品空间:优先真 products(最近 6 条),空则回退基线占位
|
||||
const sideProducts =
|
||||
products.length > 0
|
||||
@@ -889,7 +1072,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
<div className="dm-side-h">
|
||||
<div className="ti-row">
|
||||
<span className="ti">商品空间</span>
|
||||
<button className="add" type="button" title="新建商品">
|
||||
<button className="add" type="button" title="新建商品" onClick={() => navigate?.("productCreateUpload")}>
|
||||
<Plus size={11} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -916,7 +1099,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="dm-all" type="button">
|
||||
<button className="dm-all" type="button" onClick={() => navigate?.("products")}>
|
||||
<LayoutGrid size={12} />
|
||||
全部商品
|
||||
<span className="ct">{totalCount} 个</span>
|
||||
@@ -987,7 +1170,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
<div className="dm-field-h">生成张数</div>
|
||||
<div className="dm-chip-row">
|
||||
{["1 张", "2 张", "4 张", "8 张"].map((label) => (
|
||||
<button type="button" key={label} className={`dm-chip ${label === "4 张" ? "active" : ""}`}>
|
||||
<button type="button" key={label} className={`dm-chip ${label === demoCount ? "active" : ""}`} onClick={() => setDemoCount(label)}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
@@ -998,7 +1181,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
<div className="dm-field-h">画面比例</div>
|
||||
<div className="dm-chip-row">
|
||||
{["1:1", "3:4", "9:16", "16:9"].map((label) => (
|
||||
<button type="button" key={label} className={`dm-chip ${label === "3:4" ? "active" : ""}`}>
|
||||
<button type="button" key={label} className={`dm-chip ${label === demoRatio ? "active" : ""}`} onClick={() => setDemoRatio(label)}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
@@ -1016,7 +1199,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
<span>预估扣费 <span className="v">≈ ¥1.20</span></span>
|
||||
<span>余额 ¥327.40</span>
|
||||
</div>
|
||||
<button className="dm-gen" type="button">
|
||||
<button className="dm-gen" type="button" onClick={toRealTool}>
|
||||
<Sparkles size={15} />
|
||||
立即生成 · {active.title} × Ava
|
||||
</button>
|
||||
@@ -1042,9 +1225,9 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载"><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库"><Bookmark size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载" onClick={toRealTool}><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库" onClick={toRealTool}><Bookmark size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1069,9 +1252,9 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载"><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库"><Bookmark size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载" onClick={toRealTool}><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库" onClick={toRealTool}><Bookmark size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1096,7 +1279,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="取消"><X size={13} /></button>
|
||||
<button type="button" title="取消" onClick={toRealTool}><X size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1140,10 +1323,10 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
<span className="spacer" />
|
||||
<div className="dm-tb">
|
||||
<button className="icbtn" type="button" title="搜索批次"><Search size={13} /></button>
|
||||
<button className="chip" type="button">时间 <ChevronDown size={10} /></button>
|
||||
<button className="chip" type="button">状态 <ChevronDown size={10} /></button>
|
||||
<button className="chip" type="button">模特 <ChevronDown size={10} /></button>
|
||||
<button className="icbtn" type="button" title="搜索批次" onClick={toRealTool}><Search size={13} /></button>
|
||||
<button className="chip" type="button" onClick={toRealTool}>时间 <ChevronDown size={10} /></button>
|
||||
<button className="chip" type="button" onClick={toRealTool}>状态 <ChevronDown size={10} /></button>
|
||||
<button className="chip" type="button" onClick={toRealTool}>模特 <ChevronDown size={10} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1168,9 +1351,9 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载"><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库"><Bookmark size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载" onClick={toRealTool}><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库" onClick={toRealTool}><Bookmark size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1193,9 +1376,9 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载"><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库"><Bookmark size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载" onClick={toRealTool}><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库" onClick={toRealTool}><Bookmark size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1218,7 +1401,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="取消"><X size={13} /></button>
|
||||
<button type="button" title="取消" onClick={toRealTool}><X size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1245,9 +1428,9 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载"><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库"><Bookmark size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部下载" onClick={toRealTool}><Download size={13} /></button>
|
||||
<button type="button" title="加入资产库" onClick={toRealTool}><Bookmark size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dm-batch-grid">
|
||||
@@ -1274,7 +1457,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
<button type="button" title="全部重跑"><RefreshCw size={13} /></button>
|
||||
<button type="button" title="全部重跑" onClick={toRealTool}><RefreshCw size={13} /></button>
|
||||
<button type="button" title="删除"><Trash2 size={13} /></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1288,28 +1471,28 @@ export function ModelPhotoDemoPage({ variant, products, onBack }: { variant: "A"
|
||||
{/* 底部 fixed 参数面板 */}
|
||||
<div className="dm-param-wrap">
|
||||
<div className="dm-param">
|
||||
<button className="pchip active" type="button">
|
||||
<button className="pchip active" type="button" onClick={toRealTool}>
|
||||
<span className="lbl-mono">模特</span>
|
||||
<span>Ava</span>
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
<button className="pchip" type="button">
|
||||
<button className="pchip" type="button" onClick={toRealTool}>
|
||||
<span className="lbl-mono">张数</span>
|
||||
<span>4</span>
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
<button className="pchip" type="button">
|
||||
<button className="pchip" type="button" onClick={toRealTool}>
|
||||
<span className="lbl-mono">比例</span>
|
||||
<span>3:4</span>
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
<button className="pchip" type="button">
|
||||
<button className="pchip" type="button" onClick={toRealTool}>
|
||||
<span className="lbl-mono">补充提示词</span>
|
||||
<span className="muted">+ 添加</span>
|
||||
</button>
|
||||
<span className="spacer" />
|
||||
<span className="meta-right">预估 <span className="v">¥1.20</span> · 余额 <span className="v">¥327.40</span></span>
|
||||
<button className="gen-btn" type="button">
|
||||
<button className="gen-btn" type="button" onClick={toRealTool}>
|
||||
<Sparkles size={14} />
|
||||
生成 · {active.title} × Ava
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user