fix: YYX测试清单16条收口——资产库弹窗层级/批次多选删/商品筛选/模特卡单卡化/垃圾桶清空等
- 资产库:批次弹窗降层(under-confirm 990<999)修单图删除确认弹窗被压;编辑态批次卡多选批删;facets 新增 products 关联商品筛选(前后端,+2 测试);视频成品 tab 补搜索/排序/分页 - 平台套图工作台:「更多」气泡与 icon 左对齐+鼠标移出自动收起;模特卡改单卡(空态「+」占位,点卡进模特库单选回填) - 工作台:删余额条,冻结/共计移至小标题行右侧(.dash-funds-mono) - 模特库:点模特卡弹详情(形象图/标签/三视图+灯箱放大) - 垃圾桶:新增「全部恢复」/「清空垃圾桶」(清空二次确认) - 视频项目页:字重/阴影等轻量对齐设计规范 - api.assetFacets 返回体逐键兜底:旧后端缺 products 键会致资产库整页 undefined.find 白屏(前后端部署时序防护,浏览器已复现验证) 验证:tsc --noEmit / vite build / apps.assets 测试全过;暂存树已独立编译验证,不依赖工作区其余在途改动 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -547,14 +547,6 @@ const IMAGE_SUGGESTIONS = [
|
||||
"电影感都市夜景,街道湿漉漉反射霓虹,4K 高清"
|
||||
];
|
||||
|
||||
/* 模特上身图 · 真人模特默认占位卡(基线 model-photo Ava/Luna/Mia/Zoe) */
|
||||
const FALLBACK_MODELS = [
|
||||
{ id: "m1", name: "Ava", tag: "亚洲·25岁·清新" },
|
||||
{ id: "m2", name: "Luna", tag: "亚洲·22岁·学生" },
|
||||
{ id: "m3", name: "Mia", tag: "混血·28岁·OL" },
|
||||
{ id: "m4", name: "Zoe", tag: "亚洲·30岁·健身" }
|
||||
];
|
||||
|
||||
/* 平台套图 · 平台卡(YYX#row10:用真平台 logo,素材在 public/assets/svg;
|
||||
logo 留作图加载失败时的兜底字符,img 为真 logo) */
|
||||
const PLATFORM_OPTIONS = [
|
||||
@@ -705,18 +697,15 @@ export function ImageWorkbenchPage({
|
||||
const resultsEndRef = useRef<HTMLDivElement | null>(null);
|
||||
// 生成结果图片放大预览
|
||||
const [preview, setPreview] = useState<{ src: string; name: string } | null>(null);
|
||||
// 模特/平台 工作台头部:搜索 + 时间排序 + 模特筛选(对左侧网格真实生效)
|
||||
// 平台套图头部:按提示词搜索已生成的结果图(gridQuery / searchOpen 仅 cover 用;
|
||||
// row40 后模特模式改单卡选择,已无网格可搜/排序,时间排序/模特筛选下拉一并移除)
|
||||
const [gridQuery, setGridQuery] = useState("");
|
||||
const [gridSort, setGridSort] = useState<"recent" | "name">("recent");
|
||||
const [tbOpen, setTbOpen] = useState<"" | "time" | "model">("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
// YYX#17:左侧商品栏折叠(收起后只剩商品图)
|
||||
const [sideCollapsed, setSideCollapsed] = useState(false);
|
||||
// YYX#12:平台套图结果筛选弹窗 —— 按平台筛选已生成的图(空集 = 不筛)
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [platformFilter, setPlatformFilter] = useState<string[]>([]);
|
||||
// 模特网格默认只露 6 个;「全部模特 →」展开后露全部,再点收起(超过 6 个时第 7 个之后本来选不到)
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
// 左侧商品空间搜索(原 input 无 state/无过滤=死框,输入无效);按名称/分类过滤
|
||||
const [prodQuery, setProdQuery] = useState("");
|
||||
const visibleProducts = products.filter((item) => {
|
||||
@@ -1336,7 +1325,10 @@ export function ImageWorkbenchPage({
|
||||
<button className="gen-img-btn" type="button" title="下载" onClick={() => downloadImage(url, `${meta.title}-${index + 1}.png`)}>
|
||||
<Download size={14} />
|
||||
</button>
|
||||
<div className="gen-img-more">
|
||||
{/* row39:鼠标移出「更多按钮 + 气泡」整体区域自动收起(气泡是本容器后代,悬停气泡不算移出;
|
||||
按钮与气泡间 6px 空隙由 .gen-bubble::before 透明桥接补上,移过去不会误收起)。
|
||||
仅当收起的是本格气泡才清空,避免误关他格。保留点击切换 + 点空白收起。 */}
|
||||
<div className="gen-img-more" onMouseLeave={() => setOpenMore((k) => (k === cellKey ? "" : k))}>
|
||||
<button className="gen-img-btn" type="button" title="更多" onClick={() => setOpenMore((k) => (k === cellKey ? "" : cellKey))}>
|
||||
<MoreHorizontal size={14} />
|
||||
</button>
|
||||
@@ -1783,22 +1775,25 @@ export function ImageWorkbenchPage({
|
||||
商品库
|
||||
</button> */}
|
||||
<span className="spacer" />
|
||||
<div className="tb-search-wrap">
|
||||
{searchOpen && (
|
||||
<div className="iw-search-box">
|
||||
<input className="input" autoFocus placeholder={mode === "model" ? "搜索模特" : "搜索图片提示词"} value={gridQuery} onChange={(event) => setGridQuery(event.target.value)} />
|
||||
{/* YYX#12:输入框内 × 清空已输入内容 */}
|
||||
{gridQuery && (
|
||||
<button className="iw-search-clear" type="button" aria-label="清空搜索" onClick={() => setGridQuery("")}>
|
||||
<X size={13} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button className={`search-btn${searchOpen ? " active" : ""}`} type="button" title={mode === "model" ? "搜索批次/模特" : "搜索"} onClick={() => { setSearchOpen((v) => !v); if (searchOpen) setGridQuery(""); }}>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/* 搜索仅平台套图用(按提示词筛结果图);模特模式改单卡「+」选择,无网格可搜,不再显示 */}
|
||||
{mode === "cover" && (
|
||||
<div className="tb-search-wrap">
|
||||
{searchOpen && (
|
||||
<div className="iw-search-box">
|
||||
<input className="input" autoFocus placeholder="搜索图片提示词" value={gridQuery} onChange={(event) => setGridQuery(event.target.value)} />
|
||||
{/* YYX#12:输入框内 × 清空已输入内容 */}
|
||||
{gridQuery && (
|
||||
<button className="iw-search-clear" type="button" aria-label="清空搜索" onClick={() => setGridQuery("")}>
|
||||
<X size={13} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button className={`search-btn${searchOpen ? " active" : ""}`} type="button" title="搜索" onClick={() => { setSearchOpen((v) => !v); if (searchOpen) setGridQuery(""); }}>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* YYX#12:平台套图 —— 搜索 icon 右侧加筛选按钮,点开悬浮弹窗按平台筛选结果图 */}
|
||||
{mode === "cover" && (
|
||||
<div className={`tb-menu-wrap chip-wrap iw-filter-wrap${filterOpen ? " open" : ""}`} data-filter="platform">
|
||||
@@ -1829,26 +1824,7 @@ export function ImageWorkbenchPage({
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
{/* row40:模特模式已改单卡「+」选择,原「时间排序 / 全部模特」下拉随网格一并移除 */}
|
||||
</div>
|
||||
|
||||
<div className="iw-main-body">
|
||||
@@ -1859,65 +1835,40 @@ export function ImageWorkbenchPage({
|
||||
<div className="iw-step-h">
|
||||
<span className="num">1</span>
|
||||
<span className="title">选择模特</span>
|
||||
{/* P0①:接入现成模特库全屏弹窗(原「全部模特→」仅内联展开;现可打开完整演员库选用) */}
|
||||
<button className="iw-actor-lib-btn" type="button" onClick={() => setActorLibOpen(true)}>
|
||||
<Users size={12} />
|
||||
模特库
|
||||
</button>
|
||||
{personAssets.length > 6 && (
|
||||
<span className="right" role="button" tabIndex={0} style={{ cursor: "pointer" }} onClick={() => setShowAllModels((v) => !v)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setShowAllModels((v) => !v); } }}>{showAllModels ? "收起 ↑" : `全部 (${personAssets.length})`}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="model-grid">
|
||||
{personAssets.length > 0
|
||||
? 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, showAllModels ? undefined : 6)
|
||||
.map((item) => {
|
||||
const url = item.files?.[0]?.preview_url;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
className={`model-card ${pickedIds.includes(item.id) ? "selected" : ""}`}
|
||||
onClick={() => togglePick(item.id, item.name)}
|
||||
>
|
||||
<span className="m-check">
|
||||
<Check size={11} />
|
||||
</span>
|
||||
<div className="m-thumb">
|
||||
{url ? (
|
||||
<img className="m-thumb-img" src={url} alt={item.name} loading="lazy" />
|
||||
) : (
|
||||
<div className="placeholder">
|
||||
<span className="ph-frame">{item.name.slice(0, 4)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-name">{item.name}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
: 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}
|
||||
className={`model-card ${pickedIds.includes(item.id) ? "selected" : ""}`}
|
||||
onClick={() => togglePick(item.id, item.name)}
|
||||
>
|
||||
<span className="m-check">
|
||||
<Check size={11} />
|
||||
</span>
|
||||
<div className="placeholder m-thumb">
|
||||
<span className="ph-frame">{item.name}</span>
|
||||
<div className="m-name">{item.name}</div>
|
||||
{/* row40:模特改单选 —— 只显示一张卡。已选则展示该模特(点卡片换模特);
|
||||
未选则「+」占位卡。两者点击都打开模特库(ActorLibrary,mode=replace),
|
||||
onPick 回填 pickedIds=[id] 单选,提交逻辑不变。 */}
|
||||
<div className="model-grid model-grid-single">
|
||||
{(() => {
|
||||
const selected = pickedIds[0] ? personAssets.find((a) => a.id === pickedIds[0]) : undefined;
|
||||
const url = selected?.files?.[0]?.preview_url;
|
||||
const name = selected?.name || pickedModelName;
|
||||
if (pickedIds[0]) {
|
||||
return (
|
||||
<button type="button" className="model-card selected" title="点击打开模特库更换模特" onClick={() => setActorLibOpen(true)}>
|
||||
<div className="m-thumb">
|
||||
{url ? (
|
||||
<img className="m-thumb-img" src={url} alt={name} loading="lazy" />
|
||||
) : (
|
||||
<div className="placeholder">
|
||||
<span className="ph-frame">{(name || "模特").slice(0, 4)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="m-name">{name || "已选模特"}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button type="button" className="model-card model-card-empty" title="从模特库选择模特" onClick={() => setActorLibOpen(true)}>
|
||||
<div className="m-thumb m-thumb-empty">
|
||||
<Plus size={26} />
|
||||
<span className="m-empty-label">从模特库选择</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1950,7 +1901,7 @@ export function ImageWorkbenchPage({
|
||||
<span className="title">生成设置</span>
|
||||
</div>
|
||||
<div className="iw-sub">
|
||||
<div className="iw-sub-h">// 生成数量{mode === "model" ? " (每模特)" : ""}</div>
|
||||
<div className="iw-sub-h">// 生成数量</div>
|
||||
<div className="pill-row">
|
||||
{countOptions.map((value) => (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user