feat(pipeline): 基础资产页提取闸门简化为单按钮 + 卡式 loading + 顶栏重新提取
- 进资产页遮罩上从「3 个按钮(只提取/提取+生成/全套)」简化为单个醒目 CTA「提取角色 / 场景」 (走 only 模式:只提取不出图,生成仍由用户后续手动点);提取时用脚本同款卡式 loading (spinner + 标题 + 跳点 + 扫光进度条;sgv-* 样式解作用域以复用) - 内容区右上角(团队头像正下方)加「重新提取」按钮 —— 仅基础资产页且已提取过时露出,点击重提 验收:tsc+build 绿;无头核对(空项目显单按钮闸门「提取角色/场景」、已提取项目显「重新提取」、0 console error) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -151,17 +151,17 @@
|
||||
/* 行: 生成时左侧脚本区罩子(整体)/ 单镜罩子 + 转圈 */
|
||||
.script-gen-veil { position: absolute; inset: 0; z-index: 6; display: flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--background-base) 86%, transparent); }
|
||||
/* 方案 B · 品牌生成卡:icon-box 转圈 + 标题 + 动态点 + 橙色扫光进度条 */
|
||||
.script-gen-veil .sgv-card { display: flex; align-items: center; gap: 12px; padding: 14px 18px; min-width: 252px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: var(--shadow-floating); }
|
||||
.script-gen-veil .sgv-ico { width: 32px; height: 32px; flex: 0 0 32px; display: grid; place-items: center; background: var(--heat-12); border-radius: var(--r-sm); }
|
||||
.script-gen-veil .sgv-ico .spinner { width: 18px; height: 18px; border: 2.5px solid var(--heat-20); border-top-color: var(--heat); border-radius: 50%; animation: assetSpin .7s linear infinite; }
|
||||
.script-gen-veil .sgv-body { flex: 1; display: flex; flex-direction: column; gap: 9px; }
|
||||
.script-gen-veil .sgv-title { font-size: 13px; font-weight: 500; color: var(--ink); display: inline-flex; align-items: center; gap: 4px; }
|
||||
.script-gen-veil .sgv-dots { display: inline-flex; gap: 3px; align-items: center; }
|
||||
.script-gen-veil .sgv-dots i { width: 3px; height: 3px; border-radius: 50%; background: var(--heat); animation: sgvBlink 1.2s infinite; }
|
||||
.script-gen-veil .sgv-dots i:nth-child(2) { animation-delay: .2s; }
|
||||
.script-gen-veil .sgv-dots i:nth-child(3) { animation-delay: .4s; }
|
||||
.script-gen-veil .sgv-bar { position: relative; height: 4px; border-radius: 999px; background: var(--heat-12); overflow: hidden; }
|
||||
.script-gen-veil .sgv-bar::after { content: ""; position: absolute; top: 0; bottom: 0; width: 42%; border-radius: 999px; background: var(--heat); animation: sgvBar 1.15s ease-in-out infinite; }
|
||||
.sgv-card { display: flex; align-items: center; gap: 12px; padding: 14px 18px; min-width: 252px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); box-shadow: var(--shadow-floating); }
|
||||
.sgv-ico { width: 32px; height: 32px; flex: 0 0 32px; display: grid; place-items: center; background: var(--heat-12); border-radius: var(--r-sm); }
|
||||
.sgv-ico .spinner { width: 18px; height: 18px; border: 2.5px solid var(--heat-20); border-top-color: var(--heat); border-radius: 50%; animation: assetSpin .7s linear infinite; }
|
||||
.sgv-body { flex: 1; display: flex; flex-direction: column; gap: 9px; }
|
||||
.sgv-title { font-size: 13px; font-weight: 500; color: var(--ink); display: inline-flex; align-items: center; gap: 4px; }
|
||||
.sgv-dots { display: inline-flex; gap: 3px; align-items: center; }
|
||||
.sgv-dots i { width: 3px; height: 3px; border-radius: 50%; background: var(--heat); animation: sgvBlink 1.2s infinite; }
|
||||
.sgv-dots i:nth-child(2) { animation-delay: .2s; }
|
||||
.sgv-dots i:nth-child(3) { animation-delay: .4s; }
|
||||
.sgv-bar { position: relative; height: 4px; border-radius: 999px; background: var(--heat-12); overflow: hidden; }
|
||||
.sgv-bar::after { content: ""; position: absolute; top: 0; bottom: 0; width: 42%; border-radius: 999px; background: var(--heat); animation: sgvBar 1.15s ease-in-out infinite; }
|
||||
.shot-gen-veil { position: absolute; inset: 0; z-index: 4; display: flex; gap: 8px; align-items: center; justify-content: center; background: color-mix(in srgb, var(--background-base) 80%, transparent); border-radius: var(--r-sm); }
|
||||
.shot-gen-veil .mono { font-size: 12px; color: var(--ink-3); letter-spacing: .04em; }
|
||||
.script-gen-veil .spinner, .shot-gen-veil .spinner { width: 26px; height: 26px; border: 2.5px solid var(--heat-20); border-top-color: var(--heat); border-radius: 50%; animation: assetSpin .7s linear infinite; flex: 0 0 auto; }
|
||||
@@ -855,3 +855,8 @@
|
||||
.rg-actions .btn-primary { flex: 1; }
|
||||
.rg-force { display: inline-flex; align-items: baseline; gap: 5px; }
|
||||
.rg-warn { font-size: 10px; font-family: var(--font-mono); color: var(--black-alpha-48); }
|
||||
|
||||
/* 基础资产页:右上角(头像正下方)重新提取按钮 */
|
||||
.pipeline-reextract { position: absolute; top: 14px; right: 28px; z-index: 5; }
|
||||
/* 提取闸门单按钮:醒目主 CTA */
|
||||
.eg-extract-btn { margin-top: 24px; min-width: 220px; }
|
||||
|
||||
@@ -2222,6 +2222,12 @@ export function PipelinePage(props: {
|
||||
|
||||
<div className="content content--fh content--fh-flat" id="page-content">
|
||||
<CornerMarks />
|
||||
{/* 基础资产页:右上角(头像正下方)重新提取角色/场景 —— 仅本阶段且已提取过时露出 */}
|
||||
{viewStage === 2 && project.metadata?.entities_extracted === true && (
|
||||
<button type="button" className="btn btn-ghost btn-sm pipeline-reextract" onClick={() => void runExtract("only")} disabled={extractState === "running"}>
|
||||
{extractState === "running" ? "提取中…" : "重新提取"}
|
||||
</button>
|
||||
)}
|
||||
{notice && <ToastLike notice={notice} />}
|
||||
|
||||
{/* ============= STAGE 1 · 脚本 ============= */}
|
||||
@@ -2573,29 +2579,18 @@ export function PipelinePage(props: {
|
||||
<div className="extract-gate">
|
||||
<div className="extract-gate-panel">
|
||||
{extractState === "running" ? (
|
||||
<div className="extract-gate-loading">
|
||||
<span className="eg-spinner" aria-hidden="true"></span>
|
||||
<div className="eg-load-msg">{extractMsg || "处理中…"}</div>
|
||||
<div className="mono eg-load-sub">// 蒙版盖住别动,完成自动揭开</div>
|
||||
<div className="sgv-card">
|
||||
<span className="sgv-ico"><span className="spinner"></span></span>
|
||||
<div className="sgv-body">
|
||||
<div className="sgv-title">{extractMsg || "正在提取角色 / 场景"}<span className="sgv-dots"><i></i><i></i><i></i></span></div>
|
||||
<div className="sgv-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="eg-title">先从剧本认出角色 / 场景</div>
|
||||
<div className="mono eg-sub">// 生成图会扣费,交给你点,不自动花钱</div>
|
||||
<div className="extract-gate-btns">
|
||||
<button type="button" className="eg-btn" onClick={() => void runExtract("only")}>
|
||||
<span className="eg-btn-head"><span className="eg-btn-main">只提取关键词</span><span className="eg-tag eg-tag-free">不出图</span></span>
|
||||
<span className="eg-btn-sub">AI 认出角色 / 场景,出提示词,先不生成</span>
|
||||
</button>
|
||||
<button type="button" className="eg-btn" onClick={() => void runExtract("gen")}>
|
||||
<span className="eg-btn-head"><span className="eg-btn-main">提取 + 生成角色和场景</span><span className="eg-tag eg-tag-pay">会扣费</span></span>
|
||||
<span className="eg-btn-sub">提完顺手生成角色立绘 + 场景图</span>
|
||||
</button>
|
||||
<button type="button" className="eg-btn eg-btn-full" onClick={() => void runExtract("full")}>
|
||||
<span className="eg-btn-head"><span className="eg-btn-main">提取 + 全套资产</span><span className="eg-tag eg-tag-pay">会扣费 · 最全</span></span>
|
||||
<span className="eg-btn-sub">角色全身照 + 三视图 + 场景图,一步到位</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mono eg-sub">// AI 认出角色 / 场景并出提示词,稍后你再逐个生成,不自动花钱</div>
|
||||
<button type="button" className="btn btn-primary btn-lg eg-extract-btn" onClick={() => void runExtract("only")}>提取角色 / 场景</button>
|
||||
{extractErr && <div className="eg-err">{extractErr}</div>}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { chromium } from "playwright";
|
||||
import path from "node:path"; import fs from "node:fs";
|
||||
const OUT=path.resolve("../../../_qa_shots/gate"); fs.mkdirSync(OUT,{recursive:true});
|
||||
const b=await chromium.launch({headless:true}); const r={consoleErrors:[]};
|
||||
const p=await (await b.newContext({viewport:{width:1440,height:900}})).newPage();
|
||||
p.on("console",m=>{if(m.type()==="error")r.consoleErrors.push(m.text())}); p.on("pageerror",e=>r.consoleErrors.push("PE:"+e.message));
|
||||
await p.goto("http://localhost:5200/login"); await p.waitForTimeout(300);
|
||||
await p.fill("#auth-username","airshelf"); await p.fill("#auth-pwd","Restraint2026");
|
||||
await p.click("button.btn-cta"); await p.waitForFunction(()=>!location.pathname.startsWith("/login"),{timeout:12000});
|
||||
// ① 未提取项目 → 闸门单按钮
|
||||
await p.goto("http://localhost:5200/pipeline/f0eef1aa-5182-438e-b44c-f549326f7fd8"); await p.waitForTimeout(2200);
|
||||
await p.locator('a[data-stage="2"]').first().click().catch(()=>{}); await p.waitForTimeout(2000);
|
||||
r.gateVisible=await p.locator(".extract-gate").count();
|
||||
r.gateBtns=await p.locator(".extract-gate-btns button, .eg-extract-btn").count();
|
||||
r.gateBtnText=await p.locator(".eg-extract-btn").innerText().catch(()=>"");
|
||||
await p.screenshot({path:path.join(OUT,"gate-single.png")});
|
||||
// ② 已提取项目 → 重新提取按钮
|
||||
await p.goto("http://localhost:5200/pipeline/bdbf39d3-1a89-4135-b1b7-26d3331a7021"); await p.waitForTimeout(2200);
|
||||
await p.locator('a[data-stage="2"]').first().click().catch(()=>{}); await p.waitForTimeout(2000);
|
||||
r.reextractBtn=await p.locator(".pipeline-reextract").count();
|
||||
r.reextractText=await p.locator(".pipeline-reextract").innerText().catch(()=>"");
|
||||
await p.screenshot({path:path.join(OUT,"reextract.png")});
|
||||
await b.close(); console.log(JSON.stringify(r,null,2));
|
||||
Reference in New Issue
Block a user