From f0b85dddd9eceda17187802a069b0e36cfc8655d Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 20 May 2026 18:40:30 +0800 Subject: [PATCH] fix: merge pack progress into project brief --- .gitignore | 1 + src/app/globals.css | 110 ++++++++++++++++++++++- src/app/page.tsx | 163 ++++++++++++++++++++++++++++++++--- src/components/PackPanel.tsx | 157 +++++---------------------------- 4 files changed, 282 insertions(+), 149 deletions(-) diff --git a/.gitignore b/.gitignore index aa79fd9..607c604 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ __pycache__/ # UI 验证产物(Playwright MCP / 手动截图),不入库 .playwright-mcp/ +.playwright-cli/ ui-*.png diff --git a/src/app/globals.css b/src/app/globals.css index 151c55f..6de13b2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -479,7 +479,7 @@ input, textarea { .project-board-grid { display: grid; - grid-template-columns: clamp(330px, 22vw, 410px) minmax(0, 1fr) 88px; + grid-template-columns: clamp(360px, 24vw, 450px) minmax(0, 1fr) 88px; gap: 14px; min-height: 0; flex: 1; @@ -606,6 +606,112 @@ input, textarea { padding: 13px; } +.project-pack-row { + display: grid; + grid-template-columns: 24px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + border-radius: 8px; + background: rgba(255,255,255,0.035); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07); + padding: 9px; +} + +.project-pack-row--locked { + opacity: 0.58; +} + +.project-pack-index { + display: grid; + width: 24px; + height: 24px; + place-items: center; + border-radius: 8px; + background: linear-gradient(135deg, rgba(230,245,120,0.22), rgba(214,179,106,0.18)); + box-shadow: inset 0 0 0 1px rgba(230,245,120,0.18); + color: #e6f578; + font-size: 10px; + font-weight: 700; +} + +.project-pack-status { + white-space: nowrap; + border-radius: 999px; + background: rgba(255,255,255,0.045); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); + padding: 3px 6px; + color: rgba(255,255,255,0.44); + font-size: 10px; + line-height: 1; +} + +.project-pack-status--done { + background: rgba(230,245,120,0.13); + box-shadow: inset 0 0 0 1px rgba(230,245,120,0.22); + color: #e6f578; +} + +.project-pack-status--ready { + background: rgba(214,179,106,0.12); + box-shadow: inset 0 0 0 1px rgba(214,179,106,0.20); + color: #f2d38c; +} + +.project-pack-status--locked { + background: rgba(255,255,255,0.045); + color: rgba(255,255,255,0.42); +} + +.project-pack-action, +.project-pack-jump, +.project-spec-action { + display: inline-grid; + place-items: center; + min-height: 28px; + border-radius: 8px; + background: rgba(255,255,255,0.055); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); + color: rgba(255,255,255,0.62); + font-size: 10px; + transition: background 160ms ease, color 160ms ease, box-shadow 160ms ease; +} + +.project-pack-action, +.project-spec-action { + padding: 0 8px; +} + +.project-pack-jump { + width: 28px; +} + +.project-pack-action:hover:not(:disabled), +.project-pack-jump:hover, +.project-spec-action:hover:not(:disabled) { + background: rgba(255,255,255,0.10); + box-shadow: inset 0 0 0 1px rgba(230,245,120,0.18); + color: rgba(255,255,255,0.92); +} + +.project-pack-action:disabled, +.project-spec-action:disabled { + cursor: not-allowed; + opacity: 0.38; +} + +.project-pack-action:focus-visible, +.project-pack-jump:focus-visible, +.project-spec-action:focus-visible { + outline: 2px solid rgba(230,245,120,0.45); + outline-offset: 2px; +} + +.project-spec-action--primary { + background: rgba(230,245,120,0.13); + box-shadow: inset 0 0 0 1px rgba(230,245,120,0.22); + color: #e6f578; +} + .project-spec-row { display: grid; grid-template-columns: 42px minmax(0, 1fr); @@ -739,7 +845,7 @@ input, textarea { } .project-board-grid { - grid-template-columns: clamp(370px, 22vw, 440px) minmax(0, 1fr) 96px; + grid-template-columns: clamp(390px, 24vw, 480px) minmax(0, 1fr) 96px; gap: 18px; } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 6ad5565..ee4e55a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,7 @@ import Sidebar from '@/components/Sidebar'; import PackPanel from '@/components/PackPanel'; import ProjectGalleryDrawer from '@/components/ProjectGalleryDrawer'; import { OasisCanvas } from '@/components/login/OasisCanvas'; -import { PACK_ORDER, PACK_TEMPLATES } from '@/lib/templates'; +import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES } from '@/lib/templates'; import type { GenImage, GenSession, @@ -51,6 +51,13 @@ function packSlotTotal() { return PACK_ORDER.reduce((sum, kind) => sum + PACK_TEMPLATES[kind].length, 0); } +const PACK_BRIEF_DESCRIPTIONS: Record = { + patent: '六面视图 · 45° 立体图 · 局部放大', + accessories: '配件六视图 · 连接结构 · 尺寸 · 组合图', + production: '尺寸 · 材料 · 颜色 · 拆件 · 包装', + marketing: '白底商品图 · 场景图 · 细节图 · 社媒图', +}; + function ProjectStat({ label, value, tone }: { label: string; value: string | number; tone?: 'accent' | 'soft' }) { return (
@@ -88,7 +95,114 @@ function ReferenceStrip({ session }: { session: GenSession }) { ); } -function ProjectBrief({ session }: { session: GenSession }) { +function packForKind(session: GenSession, kind: PackKind, sourceImageId?: string) { + return session.packs?.find(pack => pack.kind === kind && (!sourceImageId || pack.sourceImageId === sourceImageId)); +} + +function isPackComplete(session: GenSession, kind: PackKind, sourceImageId?: string) { + const pack = packForKind(session, kind, sourceImageId); + return Boolean(pack && pack.assets.length >= PACK_TEMPLATES[kind].length); +} + +function ProjectPackOverview({ + session, + primaryImage, + loadingKind, + onGeneratePack, +}: { + session: GenSession; + primaryImage: GenImage | null; + loadingKind: PackKind | null; + onGeneratePack: (image: GenImage, kind: PackKind) => void; +}) { + if (!primaryImage) return null; + const sourceImage = primaryImage; + + return ( +
+
+ 生产进度 + 串行生成 +
+
+ {PACK_ORDER.map((kind, index) => { + const pack = packForKind(session, kind, sourceImage.id); + const total = PACK_TEMPLATES[kind].length; + const count = pack?.assets.length ?? 0; + const complete = count >= total; + const previousKind = index > 0 ? PACK_ORDER[index - 1] : null; + const locked = !session.characterSpec || Boolean(previousKind && !isPackComplete(session, previousKind, sourceImage.id)); + const running = loadingKind === kind; + const progress = Math.round((count / total) * 100); + + function handleGenerate() { + if (locked || running) return; + if (pack) { + const ok = window.confirm(`${PACK_LABELS[kind]} 已有 ${count} 张图,重新生成会再次调用图片模型并产生费用。确认继续?`); + if (!ok) return; + } + onGeneratePack(sourceImage, kind); + } + + return ( +
+
{index + 1}
+
+
+ {PACK_LABELS[kind]} + {PACK_BRIEF_DESCRIPTIONS[kind]} +
+
+
+
+
+ {count}/{total} +
+
+
+ + {complete ? '完成' : locked ? '锁定' : '可生成'} + + + +
+
+ ); + })} +
+
+ ); +} + +function ProjectBrief({ + session, + loadingKind, + characterLoading, + onGeneratePack, + onLockCharacter, +}: { + session: GenSession; + loadingKind: PackKind | null; + characterLoading: boolean; + onGeneratePack: (image: GenImage, kind: PackKind) => void; + onLockCharacter: (image: GenImage) => void; +}) { const selectedImages = session.images.filter(image => image.status === 'selected'); const primaryImage = selectedImages[0] ?? session.images[0] ?? null; const generatedAssets = (session.packs ?? []).reduce((sum, pack) => sum + pack.assets.length, 0); @@ -110,7 +224,7 @@ function ProjectBrief({ session }: { session: GenSession }) { {session.characterSpec?.oneLiner || session.prompt || '当前项目暂无描述'}

- + {modeLabel(session.inputMode)}
@@ -155,11 +269,28 @@ function ProjectBrief({ session }: { session: GenSession }) { + + {session.characterSpec ? (
角色设定 - 已锁定 +
+ 已锁定 + +
@@ -174,7 +305,17 @@ function ProjectBrief({ session }: { session: GenSession }) {
) : (
-
角色设定
+
+
角色设定
+ +

从右侧图库选中主方案后,在生产矩阵里锁定角色设定。

@@ -487,15 +628,17 @@ export default function Home() {
- +
diff --git a/src/components/PackPanel.tsx b/src/components/PackPanel.tsx index 715b5e2..850756f 100644 --- a/src/components/PackPanel.tsx +++ b/src/components/PackPanel.tsx @@ -154,14 +154,12 @@ function AssetRow({ template, asset, accent, onRegenerate }: { } /* ── Pack Section (collapsible) ───────────────── */ -function PackSection({ kind, pack, isLoading, locked, lockReason, stepIndex, onGenerate, onRegenerateAsset }: { +function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAsset }: { kind: PackKind; pack: AssetPack | undefined; - isLoading: boolean; locked: boolean; lockReason?: string; stepIndex: number; - onGenerate: () => void; onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise; }) { const [open, setOpen] = useState(kind === 'patent'); @@ -169,67 +167,32 @@ function PackSection({ kind, pack, isLoading, locked, lockReason, stepIndex, onG const templates = PACK_TEMPLATES[kind]; const generatedCount = pack?.assets.length ?? 0; const total = templates.length; - const progressPct = Math.round((generatedCount / total) * 100); const complete = generatedCount >= total; - function handleGenerateClick() { - if (locked) return; - if (pack) { - const ok = window.confirm(`${PACK_LABELS[kind]} 已有 ${generatedCount} 张图,重新生成会再次调用图片模型并产生费用。确认继续?`); - if (!ok) return; - } - onGenerate(); - } - return (
- {/* header — always visible */} -
+
{stepIndex}
- {PACK_LABELS[kind]} + {PACK_LABELS[kind]}明细 {PACK_DESCRIPTIONS[kind]} {complete && 完成} {locked && 锁定}
- {/* progress bar */} -
-
-
-
- {generatedCount}/{total} -
-
- {/* actions */} -
- -
+ {generatedCount}/{total} +
{locked && lockReason && ( @@ -481,20 +444,12 @@ function previousPackLabel(kind: PackKind) { export default function PackPanel({ session, - loadingKind, - characterLoading, videoLoading, - onGenerate, - onLockCharacter, onRegenerateAsset, onGenerateVideo, }: { session: GenSession; - loadingKind: PackKind | null; - characterLoading: boolean; videoLoading: boolean; - onGenerate: (image: GenImage, kind: PackKind) => void; - onLockCharacter: (image: GenImage) => void; onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise; onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => void; }) { @@ -525,89 +480,19 @@ export default function PackPanel({ ); } - const generatedTotal = packs.reduce((s, p) => s + p.assets.length, 0); const completedPackCount = PACK_ORDER.filter(kind => isPackComplete(packForKind(packs, kind, primaryImage.id), kind)).length; const imagePacksComplete = completedPackCount === PACK_ORDER.length; return ( -
- {/* Step 03 header card */} -
-
-
- Step · 03 · Lock & Generate -

串行生产流程

-

- 先锁定角色,再从专利包开始逐步生成;前一步完成后,下一步才会解锁。 -

-
- {/* primary image + stats */} -
-
-
{generatedTotal} / {totalImageSlots} 张
-
图片位
-
-
- selected -
-
主方案
-
+
+
+
+
+ Asset Details +

生成明细

+ 查看与单张重做
- - {/* action buttons */} -
- -
- 当前进度:{completedPackCount}/{PACK_ORDER.length} 个图片包完成 -
-
- - {/* CharacterSpec */} - {session.characterSpec && ( -
-
-
-
{session.characterSpec.name}
-
{session.characterSpec.oneLiner}
-
- CharacterSpec -
-
-
- {[ - ['形态', session.characterSpec.speciesShape], - ['比例', session.characterSpec.bodyRatio], - ['配色', session.characterSpec.colorPalette.join('、')], - ['材料', session.characterSpec.materials.join('、')], - ['L1', session.characterSpec.cleanReferenceImageUrl ? '白底锚图已生成' : '未生成'], - ].map(([label, value]) => ( -
- {label} - {value} -
- ))} -
-
- )} - - {/* Section nav */}
@@ -629,11 +514,9 @@ export default function PackPanel({ key={kind} kind={kind} pack={pack} - isLoading={loadingKind === kind} locked={locked} lockReason={lockReason} stepIndex={index + 1} - onGenerate={() => onGenerate(primaryImage, kind)} onRegenerateAsset={onRegenerateAsset} /> );