feat(pipeline): 未生成的人物/场景卡可点进详情(先立绘后三视图)+ 卡内提示词框固定5行
- seed(未生成·来自脚本)卡:缩略图/名字可点 → 进资产详情弹窗(保留卡上内联「AI 生成」快捷, 两条路都能生成,A 方案)。详情弹窗支持「还没生成组」的空壳实体(用脚本名+提示词兜底), 生成立绘后刷新按同名 key 自动切回真实体。 - 顺序限制(本就在详情里,现对 seed 也生效):没立绘时「AI 生成三视图」禁用 + 提示 「请先生成左侧立绘」;立绘按钮首次显示「生成立绘」(非「重跑」),替换走 seed 路由。 - 卡内提示词框(.prompt-box,外层人物/场景卡)固定 5 行高、超长框内滚动,不再被长提示词撑高。 自检:复用现成详情弹窗与 .btn/.prompt-box 共享类,无裸色值/无重写共享类;单橙锚点不变。 验证:tsc + vite build 绿;无头(独立 sqlite 栈)真点 seed 卡 → 详情打开、三视图禁用+提示、 立绘按钮「生成立绘」、提示词框 115px 固定且超长可滚动、0 控制台错误(截图存档)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e5b3833ba5
commit
be4bc1c8f4
@@ -375,7 +375,8 @@
|
||||
.prompt-box[contenteditable="true"] { cursor: text; outline: none; }
|
||||
.prompt-box[contenteditable="true"]:hover { border-color: var(--heat-20); }
|
||||
.prompt-box[contenteditable="true"]:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
|
||||
.fail-icon { width: 28px; height: 28px; background: var(--accent-crimson); color: var(--accent-white); display: grid; place-items: center; font-weight: 600; font-size: 16px; border-radius: 50%; }
|
||||
/* 外层人物/场景卡提示词框:固定 5 行高(行高 1.55 + 内边距/边框),超长不撑高、框内上下滚动 */
|
||||
.prompt-box { height: calc(5 * 1.55em + 22px); overflow-y: auto; box-sizing: border-box; }
|
||||
|
||||
/* ============= STAGE 3 · 故事板 ============= */
|
||||
.content--fh .stage[data-stage-pane="3"].active > .stage-storyboard { flex: 1 1 0; min-height: 0; overflow-y: auto; }
|
||||
|
||||
@@ -799,6 +799,14 @@ export function PipelinePage(props: {
|
||||
setAdTriId(null); // 跟随当前立绘的最新三视图
|
||||
setAdPrompt(entity.group.prompt ?? "");
|
||||
}
|
||||
// seed(未生成)卡也能点进详情:还没组,用脚本名(=key)+ 当前提示词开一个「空壳」详情。
|
||||
// 进去先生成立绘;三视图按既有逻辑(无立绘则禁用)天然挡住。生成完刷新 → 同 key(=名字)自动切到真实体。
|
||||
function openSeedDetail(kind: "person" | "scene", tag: string, promptValue: string) {
|
||||
setAdDetail({ kind, key: tag });
|
||||
setAdPortraitId(null);
|
||||
setAdTriId(null);
|
||||
setAdPrompt(promptValue);
|
||||
}
|
||||
// 流程步骤4 · 演员库覆盖层:replace 带要替换的目标(已生成卡=groupId;seed 占位卡还没组=kind+label);
|
||||
// browse 带 studio 直接进「添加人物工作台」
|
||||
const [actorLib, setActorLib] = useState<{ mode: "browse" | "replace"; studio?: boolean; groupId?: string; seedKind?: "person" | "scene"; seedLabel?: string; addNew?: boolean } | null>(null);
|
||||
@@ -2819,13 +2827,14 @@ export function PipelinePage(props: {
|
||||
const busy = isBusy(seedKey) || isBusy(`${seedKey}:tri`) || pendingHas(kind, tag);
|
||||
return (
|
||||
<div className="asset-card-2 asset-seed" data-asset-kind={kind} data-seed-tag={tag} key={seedKey}>
|
||||
<div className="placeholder thumb-2">
|
||||
<div className="placeholder thumb-2" role="button" tabIndex={0} style={{ cursor: "pointer" }} title="点击进详情(先生成立绘,再生成三视图)"
|
||||
onClick={() => openSeedDetail(kind, tag, promptValue)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openSeedDetail(kind, tag, promptValue); } }}>
|
||||
{busy
|
||||
? <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}><span className="spinner" aria-hidden="true"></span><span className="ph-frame">生成中 · 约 8s</span></div>
|
||||
: <span className="ph-frame">{tag} · 待生成</span>}
|
||||
</div>
|
||||
<div className="body-2">
|
||||
<div className="hstack"><strong style={{ fontSize: "13.5px" }}>{tag}</strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span>来自脚本</span></div>
|
||||
<div className="hstack"><strong style={{ fontSize: "13.5px", cursor: "pointer" }} onClick={() => openSeedDetail(kind, tag, promptValue)}>{tag}</strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span>来自脚本</span></div>
|
||||
<PromptBox key={seedKey} value={promptValue} onChange={(v) => setAssetPromptDraft((m) => ({ ...m, [seedKey]: v }))} />
|
||||
<div className="hstack" style={{ marginTop: 10 }}>
|
||||
<button className="btn btn-ghost btn-sm" type="button" data-stop disabled={busy} onClick={() => openActorReplaceSeed(kind, tag)}>替换</button>
|
||||
@@ -3612,8 +3621,15 @@ export function PipelinePage(props: {
|
||||
{/* ── 流程步骤4 · 人物/场景详情弹窗:立绘 + 三视图(人物)+ 提示词重获 + 版本历史 + 应用到当前项目 ── */}
|
||||
{adDetail && (() => {
|
||||
const entities = buildEntities(adDetail.kind);
|
||||
const entity = entities.find((e) => e.key === adDetail.key);
|
||||
if (!entity) return null;
|
||||
const realEntity = entities.find((e) => e.key === adDetail.key);
|
||||
// seed(还没生成组)也能开详情:用脚本名(=key)+ 当前提示词兜个「空壳」实体,进去先生成立绘。
|
||||
// 生成完刷新 → buildEntities 按同 key(label=名字)收到真实组,自动切回真实体。
|
||||
const entity: AssetEntity = realEntity ?? {
|
||||
key: adDetail.key, label: adDetail.key, name: adDetail.key, kind: adDetail.kind,
|
||||
group: { id: "", kind: adDetail.kind, prompt: adPrompt, adopted_asset: null, candidate_assets: [] },
|
||||
versions: [],
|
||||
};
|
||||
const isSeed = !realEntity; // 空壳态:还没立绘
|
||||
const isPerson = adDetail.kind === "person";
|
||||
const grp = entity.group;
|
||||
const portraitVersions = grp.candidate_assets ?? [];
|
||||
@@ -3626,7 +3642,8 @@ export function PipelinePage(props: {
|
||||
const viewTriAsset = (adTriId && triVersions.includes(adTriId)) ? adTriId : (triGroup?.adopted_asset || triVersions.at(-1) || "");
|
||||
const triUrl = candUrl(triGroup, viewTriAsset);
|
||||
const pBK = `addet-portrait:${entity.key}`;
|
||||
const busyPortrait = isBusy(pBK);
|
||||
// 没立绘时(尤其 seed 首次生成),立绘在 worker 跑 ~30s,本地 isBusy 早已落回 → 用 pendingHas 兜底转圈
|
||||
const busyPortrait = isBusy(pBK) || (!portraitUrl && pendingHas(adDetail.kind, entity.name));
|
||||
const busyTri = isBusy(`addet-tri:${entity.key}`) || isBusy(`${pBK}:tri`);
|
||||
async function regenPortrait() {
|
||||
const prompt = adPrompt.trim() || grp.prompt || `${entity!.name},9:16 竖屏`;
|
||||
@@ -3701,7 +3718,7 @@ export function PipelinePage(props: {
|
||||
{!triUrl && !busyTri && (
|
||||
<div className="asset-detail-tip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
|
||||
<span>三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。</span>
|
||||
<span>{viewPortraitAsset ? "三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。" : "请先生成左侧「立绘」,有了立绘才能据它生成三视图。"}</span>
|
||||
<button className="ai-gen-btn" type="button" disabled={busyTri || !viewPortraitAsset} onClick={() => void regenTri()}>AI 生成三视图</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -3725,9 +3742,9 @@ export function PipelinePage(props: {
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 10, justifyContent: "flex-end" }}>
|
||||
<button className="btn btn-primary btn-sm" type="button" disabled={busyPortrait} onClick={() => void regenPortrait()}>
|
||||
{busyPortrait ? <span className="asset-spinner sm" aria-hidden="true" style={{ marginRight: 4 }}></span> : <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: 4 }}><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>}
|
||||
{busyPortrait ? "生成中…" : (isPerson ? "重跑立绘" : "重跑")}
|
||||
{busyPortrait ? "生成中…" : (portraitVersions.length === 0 ? (isPerson ? "生成立绘" : "生成场景图") : (isPerson ? "重跑立绘" : "重跑"))}
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" type="button" onClick={() => openActorReplace(entity)}>替换</button>
|
||||
<button className="btn btn-ghost btn-sm" type="button" onClick={() => isSeed ? openActorReplaceSeed(adDetail.kind, entity.name) : openActorReplace(entity)}>替换</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user