auto-save 2026-05-19 08:46 (~3)
This commit is contained in:
@@ -286,6 +286,13 @@
|
||||
"message": "auto-save 2026-05-19 00:56 (~5)",
|
||||
"hash": "0517892",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T08:40:59+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-19 08:40 (~3)",
|
||||
"hash": "167bf61",
|
||||
"files_changed": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import type { AssetPack, AssetTemplate, GenImage, GenSession, PackKind, ToyAsset } from '@/lib/types';
|
||||
import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEMPLATE_SLOT_SUMMARY, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates';
|
||||
import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates';
|
||||
|
||||
const PACK_DESCRIPTIONS: Record<PackKind, string> = {
|
||||
patent: '六面视图、45° 立体图、局部放大——外观专利素材',
|
||||
accessories: '配件六视图、连接结构、尺寸和组合关系——独立配件保护 / 打样',
|
||||
production: '尺寸、材料、颜色、拆件、包装——工厂报价 / 打样',
|
||||
marketing: '白底商品图、场景图、细节图、社媒图——新品宣发',
|
||||
patent: '六面视图 · 45° 立体图 · 局部放大',
|
||||
accessories: '配件六视图 · 连接结构 · 尺寸 · 组合图',
|
||||
production: '尺寸 · 材料 · 颜色 · 拆件 · 包装',
|
||||
marketing: '白底商品图 · 场景图 · 细节图 · 社媒图',
|
||||
};
|
||||
|
||||
const PACK_ACCENT: Record<PackKind, { ring: string; chip: string; dot: string; bar: string; soft: string }> = {
|
||||
patent: {
|
||||
ring: 'ring-violet-400/30',
|
||||
chip: 'bg-violet-500/15 text-violet-200 border-violet-400/30',
|
||||
dot: 'bg-violet-400',
|
||||
bar: 'from-violet-400 to-indigo-400',
|
||||
soft: 'from-violet-500/10 to-transparent',
|
||||
},
|
||||
production: {
|
||||
ring: 'ring-amber-400/30',
|
||||
chip: 'bg-amber-500/15 text-amber-200 border-amber-400/30',
|
||||
dot: 'bg-amber-400',
|
||||
bar: 'from-amber-400 to-orange-400',
|
||||
soft: 'from-amber-500/10 to-transparent',
|
||||
},
|
||||
accessories: {
|
||||
ring: 'ring-sky-400/30',
|
||||
chip: 'bg-sky-500/15 text-sky-200 border-sky-400/30',
|
||||
dot: 'bg-sky-400',
|
||||
bar: 'from-sky-400 to-cyan-400',
|
||||
soft: 'from-sky-500/10 to-transparent',
|
||||
},
|
||||
marketing: {
|
||||
ring: 'ring-emerald-400/30',
|
||||
chip: 'bg-emerald-500/15 text-emerald-200 border-emerald-400/30',
|
||||
dot: 'bg-emerald-400',
|
||||
bar: 'from-emerald-400 to-teal-400',
|
||||
soft: 'from-emerald-500/10 to-transparent',
|
||||
},
|
||||
const PACK_ACCENT: Record<PackKind, { bar: string; chip: string; icon: string }> = {
|
||||
patent: { bar: 'from-violet-400 to-indigo-400', chip: 'bg-violet-500/15 text-violet-200 border-violet-400/30', icon: 'P' },
|
||||
accessories: { bar: 'from-sky-400 to-cyan-400', chip: 'bg-sky-500/15 text-sky-200 border-sky-400/30', icon: 'A' },
|
||||
production: { bar: 'from-amber-400 to-orange-400', chip: 'bg-amber-500/15 text-amber-200 border-amber-400/30', icon: 'F' },
|
||||
marketing: { bar: 'from-emerald-400 to-teal-400', chip: 'bg-emerald-500/15 text-emerald-200 border-emerald-400/30', icon: 'M' },
|
||||
};
|
||||
|
||||
const ASPECT_PX: Record<AssetTemplate['aspectRatio'], string> = {
|
||||
'1:1': '1024 × 1024',
|
||||
'3:4': '1024 × 1365',
|
||||
'4:5': '1024 × 1280',
|
||||
'9:16': '1080 × 1920',
|
||||
'16:9': '1920 × 1080',
|
||||
'long': '1024 × 3200',
|
||||
'1:1': '1024×1024',
|
||||
'3:4': '1024×1365',
|
||||
'4:5': '1024×1280',
|
||||
'9:16': '1080×1920',
|
||||
'16:9': '1920×1080',
|
||||
'long': '1024×3200',
|
||||
};
|
||||
|
||||
function manifestUrl(sessionId: string, kind: PackKind, version: string) {
|
||||
return `/api/export/${sessionId}_${kind}_${version}_manifest.json`;
|
||||
}
|
||||
|
||||
function AssetRow({
|
||||
template,
|
||||
asset,
|
||||
accent,
|
||||
}: {
|
||||
/* ── Asset Row ────────────────────────────────── */
|
||||
function AssetRow({ template, asset, accent }: {
|
||||
template: AssetTemplate;
|
||||
asset: ToyAsset | undefined;
|
||||
accent: typeof PACK_ACCENT[PackKind];
|
||||
@@ -67,66 +40,60 @@ function AssetRow({
|
||||
const [showPrompt, setShowPrompt] = useState(false);
|
||||
const ready = !!asset;
|
||||
return (
|
||||
<div className="grid grid-cols-[88px_1fr_120px] gap-4 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.12] transition-all">
|
||||
<div className="relative aspect-square rounded-xl overflow-hidden bg-white/[0.035] ring-1 ring-white/[0.07]">
|
||||
<div className="grid grid-cols-[72px_1fr_auto] gap-3 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
|
||||
{/* thumbnail */}
|
||||
<div className="relative aspect-square rounded-xl overflow-hidden bg-white/[0.04] ring-1 ring-white/[0.07] flex items-center justify-center">
|
||||
{ready ? (
|
||||
<img src={asset!.url} alt={template.title} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-[9px] text-white/30">
|
||||
<div className="text-white/40 font-mono">{template.view}</div>
|
||||
<div className="mt-1 px-1.5 py-0.5 rounded bg-white/[0.05] text-[8px]">{template.aspectRatio}</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-[9px] text-white/30 font-mono">{template.aspectRatio}</span>
|
||||
</div>
|
||||
)}
|
||||
{template.required && (
|
||||
<span className="absolute top-1 right-1 px-1 py-0 rounded text-[8px] font-semibold bg-violet-500/40 text-white border border-violet-300/40 backdrop-blur">
|
||||
必备
|
||||
</span>
|
||||
{template.required && !ready && (
|
||||
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-violet-400 ring-2 ring-violet-400/30" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-white truncate">{template.title}</span>
|
||||
<span className={`chip ${accent.chip} text-[10px] py-0.5`}>{template.view}</span>
|
||||
{ready && (
|
||||
<span className="chip chip-live text-[10px] py-0.5">✓ {asset!.version}</span>
|
||||
{/* info */}
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[13px] font-medium text-white leading-snug">{template.title}</span>
|
||||
{template.required && !ready && (
|
||||
<span className="text-[9px] text-violet-300/80 uppercase tracking-widest">必备</span>
|
||||
)}
|
||||
{ready && <span className="chip chip-live text-[10px] py-0">✓</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-white/55 leading-relaxed line-clamp-2">{template.description}</p>
|
||||
<p className="text-[11px] text-white/45 leading-relaxed line-clamp-1">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPrompt(s => !s)}
|
||||
className="text-[10px] text-white/40 hover:text-violet-300 transition-colors flex items-center gap-1"
|
||||
className="text-[10px] text-white/30 hover:text-violet-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={showPrompt ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
{showPrompt ? '收起 Prompt' : '查看 Prompt'}
|
||||
Prompt
|
||||
</button>
|
||||
{showPrompt && (
|
||||
<pre className="mt-1.5 p-2.5 text-[10px] text-white/65 bg-black/40 rounded-lg ring-1 ring-white/[0.08] font-mono leading-relaxed whitespace-pre-wrap break-all max-h-32 overflow-y-auto">
|
||||
<pre className="p-2 text-[10px] text-white/60 bg-black/40 rounded-lg ring-1 ring-white/[0.07] font-mono whitespace-pre-wrap break-all max-h-28 overflow-y-auto">
|
||||
{template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end justify-between gap-1.5 text-right">
|
||||
<div className="text-[10px] font-mono text-white/55">{ASPECT_PX[template.aspectRatio]}</div>
|
||||
<div className="text-[9px] text-white/30 uppercase tracking-wider">{template.aspectRatio}</div>
|
||||
<div className={`text-[10px] mt-auto ${ready ? 'text-emerald-300' : 'text-white/35'}`}>
|
||||
{/* meta */}
|
||||
<div className="flex flex-col items-end justify-between text-right shrink-0">
|
||||
<span className="text-[10px] font-mono text-white/40">{ASPECT_PX[template.aspectRatio]}</span>
|
||||
<span className={`text-[10px] ${ready ? 'text-emerald-300' : 'text-white/25'}`}>
|
||||
{ready ? 'Ready' : '待生成'}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PackSection({
|
||||
kind,
|
||||
session,
|
||||
pack,
|
||||
isLoading,
|
||||
onGenerate,
|
||||
}: {
|
||||
/* ── Pack Section (collapsible) ───────────────── */
|
||||
function PackSection({ kind, session, primaryImage, pack, isLoading, onGenerate }: {
|
||||
kind: PackKind;
|
||||
session: GenSession;
|
||||
primaryImage: GenImage;
|
||||
@@ -134,251 +101,286 @@ function PackSection({
|
||||
isLoading: boolean;
|
||||
onGenerate: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const accent = PACK_ACCENT[kind];
|
||||
const templates = PACK_TEMPLATES[kind];
|
||||
const generatedCount = pack?.assets.length ?? 0;
|
||||
const total = templates.length;
|
||||
const progressPct = (generatedCount / total) * 100;
|
||||
const progressPct = Math.round((generatedCount / total) * 100);
|
||||
|
||||
return (
|
||||
<section className="card p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<div className={`w-9 h-9 rounded-xl bg-gradient-to-br ${accent.bar} flex items-center justify-center text-white text-xs font-bold shrink-0`}>
|
||||
{kind === 'patent' ? 'P' : kind === 'accessories' ? 'A' : kind === 'production' ? 'F' : 'M'}
|
||||
<section className="card overflow-hidden" id={`pack-${kind}`}>
|
||||
{/* header — always visible */}
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg bg-gradient-to-br ${accent.bar} flex items-center justify-center text-white text-[11px] font-bold shrink-0`}>
|
||||
{accent.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-white">{PACK_LABELS[kind]}</span>
|
||||
<span className="text-[10px] text-white/35">{PACK_DESCRIPTIONS[kind]}</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold text-white">{PACK_LABELS[kind]}</h3>
|
||||
<span className="text-[11px] text-white/40">· {total} 张</span>
|
||||
{/* progress bar */}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 h-1 rounded-full bg-white/[0.06] overflow-hidden">
|
||||
<div
|
||||
className={`h-full bg-gradient-to-r ${accent.bar} transition-all`}
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 mt-0.5">{PACK_DESCRIPTIONS[kind]}</p>
|
||||
<span className="text-[10px] font-mono text-white/35 shrink-0">{generatedCount}/{total}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* actions */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{pack && (
|
||||
<a
|
||||
href={manifestUrl(session.id, kind, pack.version)}
|
||||
className="inline-flex items-center gap-1 text-[10px] text-violet-300 hover:text-violet-200 transition-colors px-2 py-1 rounded-lg bg-white/[0.04] ring-1 ring-white/[0.08]"
|
||||
className="text-[10px] text-violet-300 hover:text-violet-200 px-2 py-1 rounded-lg bg-white/[0.04] ring-1 ring-white/[0.08] transition-colors"
|
||||
title="下载 manifest"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
manifest
|
||||
↓
|
||||
</a>
|
||||
)}
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isLoading}
|
||||
className={`${pack ? 'btn btn-outline' : 'btn btn-primary'} text-xs disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
className={`${pack ? 'btn btn-outline' : 'btn btn-primary'} text-xs px-3 py-1.5 disabled:opacity-40`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
||||
</svg>
|
||||
生成中
|
||||
</>
|
||||
) : pack ? '重新生成本包' : `生成 ${total} 张`}
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
||||
</svg>
|
||||
) : pack ? '重做' : `生成 ${total} 张`}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
title={open ? '收起' : '展开'}
|
||||
className="w-7 h-7 rounded-lg bg-white/[0.04] hover:bg-white/[0.08] flex items-center justify-center text-white/50 transition-colors"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={open ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-[10px] text-white/40">
|
||||
<span>进度</span>
|
||||
<span className="font-mono">{generatedCount} / {total}</span>
|
||||
{/* asset list — collapsible */}
|
||||
{open && (
|
||||
<div className="px-4 pb-4 space-y-2 border-t border-white/[0.05]">
|
||||
<div className="pt-3 space-y-2">
|
||||
{templates.map(template => {
|
||||
const asset = pack?.assets.find(a => a.templateId === template.id);
|
||||
return (
|
||||
<AssetRow key={template.id} template={template} asset={asset} accent={accent} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
|
||||
<div
|
||||
className={`h-full bg-gradient-to-r ${accent.bar} transition-all`}
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{templates.map(template => {
|
||||
const asset = pack?.assets.find(a => a.templateId === template.id);
|
||||
return (
|
||||
<AssetRow
|
||||
key={template.id}
|
||||
template={template}
|
||||
asset={asset}
|
||||
accent={accent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function VideoSection({
|
||||
videoLoading,
|
||||
primaryImage,
|
||||
onGenerateVideo,
|
||||
}: {
|
||||
/* ── Text Template Section (collapsible) ──────── */
|
||||
function TextTemplateSection() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section className="card overflow-hidden" id="pack-text">
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-sky-500 to-cyan-400 flex items-center justify-center text-white text-[11px] font-bold shrink-0">T</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-white">设计说明文字</span>
|
||||
<span className="text-[10px] text-white/35">专利说明 · 工厂说明 · 宣发文案 · 视频脚本</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 h-1 rounded-full bg-white/[0.06]" />
|
||||
<span className="text-[10px] font-mono text-white/35 shrink-0">0/{TEXT_TEMPLATES.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="chip bg-sky-500/15 text-sky-200 border-sky-400/30 text-[10px]">GPT Text</span>
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
title={open ? '收起' : '展开'}
|
||||
className="w-7 h-7 rounded-lg bg-white/[0.04] hover:bg-white/[0.08] flex items-center justify-center text-white/50 transition-colors"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={open ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
||||
{TEXT_TEMPLATES.map(template => {
|
||||
const isOpen = showPromptId === template.id;
|
||||
return (
|
||||
<div key={template.id} className="grid grid-cols-[72px_1fr_auto] gap-3 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
|
||||
<div className="aspect-square rounded-xl bg-gradient-to-br from-sky-500/15 to-cyan-500/15 ring-1 ring-sky-400/20 flex flex-col items-center justify-center text-sky-300 text-[9px] font-mono gap-0.5">
|
||||
<span>text</span>
|
||||
<span className="text-[8px] text-sky-400/60">{template.outputFormat}</span>
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[13px] font-medium text-white">{template.title}</span>
|
||||
<span className={`chip text-[10px] py-0 ${template.kind === 'patent' ? 'bg-violet-500/15 text-violet-200 border-violet-400/30' : template.kind === 'production' ? 'bg-amber-500/15 text-amber-200 border-amber-400/30' : template.kind === 'accessories' ? 'bg-sky-500/15 text-sky-200 border-sky-400/30' : template.kind === 'marketing' ? 'bg-emerald-500/15 text-emerald-200 border-emerald-400/30' : 'chip-neutral'}`}>
|
||||
{template.kind}
|
||||
</span>
|
||||
{template.required && <span className="text-[9px] text-violet-300/80 uppercase tracking-widest">必备</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 line-clamp-1">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPromptId(isOpen ? null : template.id)}
|
||||
className="text-[10px] text-white/30 hover:text-sky-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={isOpen ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
Prompt
|
||||
</button>
|
||||
{isOpen && (
|
||||
<pre className="p-2 text-[10px] text-white/60 bg-black/40 rounded-lg ring-1 ring-white/[0.07] font-mono whitespace-pre-wrap break-all max-h-28 overflow-y-auto">
|
||||
{template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end justify-between text-right shrink-0">
|
||||
<span className="text-[9px] text-white/25 uppercase tracking-wider">{template.outputFormat}</span>
|
||||
<span className="text-[10px] text-white/25">待生成</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Video Section (collapsible) ──────────────── */
|
||||
function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: {
|
||||
videoLoading: boolean;
|
||||
primaryImage: GenImage;
|
||||
onGenerateVideo: (image: GenImage, promptTemplate: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section className="card p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-fuchsia-500 to-violet-500 flex items-center justify-center text-white text-xs font-bold">V</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold text-white">Seedance 视频</h3>
|
||||
<span className="text-[11px] text-white/40">· {VIDEO_TEMPLATES.length} 个模板</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 mt-0.5">异步任务 · 用当前主方案出宣发 / 展示短片</p>
|
||||
<section className="card overflow-hidden" id="pack-video">
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-fuchsia-500 to-violet-500 flex items-center justify-center text-white text-[11px] font-bold shrink-0">V</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-white">Seedance 视频</span>
|
||||
<span className="text-[10px] text-white/35">异步任务 · 宣发 / 展示短片</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="flex-1 h-1 rounded-full bg-white/[0.06]" />
|
||||
<span className="text-[10px] font-mono text-white/35 shrink-0">{VIDEO_TEMPLATES.length} 个模板</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="chip chip-violet">Seedance</span>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="chip chip-violet text-[10px]">Seedance</span>
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
title={open ? '收起' : '展开'}
|
||||
className="w-7 h-7 rounded-lg bg-white/[0.04] hover:bg-white/[0.08] flex items-center justify-center text-white/50 transition-colors"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={open ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{VIDEO_TEMPLATES.map(template => {
|
||||
const open = showPromptId === template.id;
|
||||
return (
|
||||
<div key={template.id} className="grid grid-cols-[88px_1fr_120px] gap-4 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.12] transition-all">
|
||||
<div className="aspect-square rounded-xl bg-gradient-to-br from-fuchsia-500/20 to-violet-500/20 ring-1 ring-fuchsia-400/20 flex items-center justify-center text-fuchsia-200 text-[10px] font-mono">
|
||||
video
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-white truncate">{template.title}</span>
|
||||
<span className="chip bg-fuchsia-500/15 text-fuchsia-200 border-fuchsia-400/30 text-[10px] py-0.5">
|
||||
{template.duration}s
|
||||
</span>
|
||||
<span className="chip chip-neutral text-[10px] py-0.5">
|
||||
{template.ratio}
|
||||
</span>
|
||||
<span className="chip chip-neutral text-[10px] py-0.5">
|
||||
1080p
|
||||
</span>
|
||||
{open && (
|
||||
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
||||
{VIDEO_TEMPLATES.map(template => {
|
||||
const isOpen = showPromptId === template.id;
|
||||
return (
|
||||
<div key={template.id} className="grid grid-cols-[72px_1fr_auto] gap-3 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
|
||||
<div className="aspect-square rounded-xl bg-gradient-to-br from-fuchsia-500/15 to-violet-500/15 ring-1 ring-fuchsia-400/20 flex flex-col items-center justify-center text-fuchsia-300 text-[9px] font-mono gap-1">
|
||||
<span>▶</span>
|
||||
<span className="text-[8px]">{template.duration}s</span>
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[13px] font-medium text-white">{template.title}</span>
|
||||
<span className="chip chip-neutral text-[10px] py-0">{template.ratio}</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 line-clamp-1">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPromptId(isOpen ? null : template.id)}
|
||||
className="text-[10px] text-white/30 hover:text-fuchsia-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={isOpen ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
Prompt
|
||||
</button>
|
||||
{isOpen && (
|
||||
<pre className="p-2 text-[10px] text-white/60 bg-black/40 rounded-lg ring-1 ring-white/[0.07] font-mono whitespace-pre-wrap break-all max-h-28 overflow-y-auto">
|
||||
{template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-white/55 leading-relaxed line-clamp-2">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPromptId(open ? null : template.id)}
|
||||
className="text-[10px] text-white/40 hover:text-fuchsia-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={open ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
{open ? '收起 Prompt' : '查看 Prompt'}
|
||||
</button>
|
||||
{open && (
|
||||
<pre className="mt-1.5 p-2.5 text-[10px] text-white/65 bg-black/40 rounded-lg ring-1 ring-white/[0.08] font-mono leading-relaxed whitespace-pre-wrap break-all max-h-32 overflow-y-auto">
|
||||
{template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end justify-between gap-1.5">
|
||||
<div className="text-[10px] text-white/30 uppercase tracking-wider">video</div>
|
||||
<button
|
||||
onClick={() => onGenerateVideo(primaryImage, template.promptTemplate)}
|
||||
disabled={videoLoading}
|
||||
className="btn btn-primary text-[11px] px-3 py-1.5 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="self-center btn btn-primary text-[11px] px-3 py-1.5 disabled:opacity-40"
|
||||
>
|
||||
{videoLoading ? '提交中' : '提交'}
|
||||
{videoLoading ? '...' : '提交'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SlotOverview() {
|
||||
const stats = [
|
||||
{ label: '总预留位', value: TEMPLATE_SLOT_SUMMARY.totalReservedSlotCount },
|
||||
{ label: '图片/脚本资产位', value: TEMPLATE_SLOT_SUMMARY.imageAssetCount },
|
||||
{ label: '设计说明文字位', value: TEMPLATE_SLOT_SUMMARY.textAssetCount },
|
||||
{ label: '必备图位', value: TEMPLATE_SLOT_SUMMARY.requiredImageAssetCount },
|
||||
{ label: 'Seedance 视频任务', value: TEMPLATE_SLOT_SUMMARY.videoTaskCount },
|
||||
];
|
||||
/* ── Section Nav ──────────────────────────────── */
|
||||
const NAV_ITEMS = [
|
||||
{ id: 'pack-patent', label: '专利', accent: 'from-violet-400 to-indigo-400' },
|
||||
{ id: 'pack-accessories', label: '配件', accent: 'from-sky-400 to-cyan-400' },
|
||||
{ id: 'pack-production', label: '生产', accent: 'from-amber-400 to-orange-400' },
|
||||
{ id: 'pack-marketing', label: '宣发', accent: 'from-emerald-400 to-teal-400' },
|
||||
{ id: 'pack-text', label: '文字', accent: 'from-sky-500 to-cyan-400' },
|
||||
{ id: 'pack-video', label: '视频', accent: 'from-fuchsia-500 to-violet-500' },
|
||||
];
|
||||
|
||||
function SectionNav({ active, onChange }: { active: string; onChange: (id: string) => void }) {
|
||||
return (
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{stats.map(item => (
|
||||
<div key={item.label} className="rounded-2xl bg-white/[0.035] ring-1 ring-white/[0.08] p-3">
|
||||
<div className="text-lg font-semibold text-white font-mono">{item.value}</div>
|
||||
<div className="mt-1 text-[10px] text-white/40">{item.label}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 overflow-x-auto scrollbar-none">
|
||||
{NAV_ITEMS.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
onChange(item.id);
|
||||
document.getElementById(item.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}}
|
||||
className={`shrink-0 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
active === item.id
|
||||
? `text-white bg-gradient-to-r ${item.accent} shadow-sm`
|
||||
: 'text-white/45 hover:text-white/70 hover:bg-white/[0.05]'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextTemplateSection() {
|
||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section className="card p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-sky-500 to-cyan-400 flex items-center justify-center text-white text-xs font-bold">T</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base font-semibold text-white">设计说明文字</h3>
|
||||
<span className="text-[11px] text-white/40">· {TEXT_TEMPLATES.length} 个文案位</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-white/45 mt-0.5">专利说明、工厂打样说明、宣发文案、视频脚本文字包</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="chip bg-sky-500/15 text-sky-200 border-sky-400/30">GPT Text</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{TEXT_TEMPLATES.map(template => {
|
||||
const open = showPromptId === template.id;
|
||||
return (
|
||||
<div key={template.id} className="grid grid-cols-[88px_1fr_120px] gap-4 p-3 rounded-2xl bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.12] transition-all">
|
||||
<div className="aspect-square rounded-xl bg-gradient-to-br from-sky-500/20 to-cyan-500/20 ring-1 ring-sky-400/20 flex flex-col items-center justify-center text-sky-200 text-[10px] font-mono">
|
||||
<span>text</span>
|
||||
{template.required && <span className="mt-1 text-[8px] text-white/70">required</span>}
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold text-white truncate">{template.title}</span>
|
||||
<span className="chip bg-sky-500/15 text-sky-200 border-sky-400/30 text-[10px] py-0.5">{template.kind}</span>
|
||||
<span className="chip chip-neutral text-[10px] py-0.5">{template.outputFormat}</span>
|
||||
{template.required && <span className="chip chip-live text-[10px] py-0.5">必备</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-white/55 leading-relaxed line-clamp-2">{template.description}</p>
|
||||
<button
|
||||
onClick={() => setShowPromptId(open ? null : template.id)}
|
||||
className="text-[10px] text-white/40 hover:text-sky-300 transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d={open ? 'M6 9l6 6 6-6' : 'M9 6l6 6-6 6'} strokeLinecap="round" />
|
||||
</svg>
|
||||
{open ? '收起 Prompt' : '查看 Prompt'}
|
||||
</button>
|
||||
{open && (
|
||||
<pre className="mt-1.5 p-2.5 text-[10px] text-white/65 bg-black/40 rounded-lg ring-1 ring-white/[0.08] font-mono leading-relaxed whitespace-pre-wrap break-all max-h-32 overflow-y-auto">
|
||||
{template.promptTemplate}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end justify-between gap-1.5 text-right">
|
||||
<div className="text-[10px] font-mono text-white/55">{template.filenamePart}</div>
|
||||
<div className="text-[9px] text-white/30 uppercase tracking-wider">{template.outputFormat}</div>
|
||||
<div className="text-[10px] text-white/35">待生成</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
/* ── Main Export ──────────────────────────────── */
|
||||
const totalImageSlots = PACK_ORDER.reduce((s, k) => s + PACK_TEMPLATES[k].length, 0);
|
||||
|
||||
export default function PackPanel({
|
||||
session,
|
||||
@@ -401,6 +403,7 @@ export default function PackPanel({
|
||||
onLockCharacter: (image: GenImage) => void;
|
||||
onGenerateVideo: (image: GenImage, prompt: string) => void;
|
||||
}) {
|
||||
const [activeNav, setActiveNav] = useState('pack-patent');
|
||||
const selectedImages = session.images.filter(image => image.status === 'selected');
|
||||
const primaryImage = selectedImages[0] ?? null;
|
||||
const packs = session.packs ?? [];
|
||||
@@ -409,12 +412,12 @@ export default function PackPanel({
|
||||
return (
|
||||
<section className="card p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-9 h-9 rounded-xl bg-white/[0.05] border border-white/[0.08] flex items-center justify-center text-white/40 text-sm">⌗</div>
|
||||
<div className="w-8 h-8 rounded-xl bg-white/[0.05] border border-white/[0.08] flex items-center justify-center text-white/30 text-sm">⌗</div>
|
||||
<div>
|
||||
<span className="section-eyebrow">Step · 03 · Lock Character</span>
|
||||
<h2 className="mt-2 text-sm font-semibold text-white">下一步:资产清单</h2>
|
||||
<p className="mt-1 text-[11px] text-white/45 leading-relaxed">
|
||||
先在上方九宫格选中一个主方案,下面会展开完整预设资产位(专利 / 生产 / 宣发 / 视频)。
|
||||
<span className="section-eyebrow">Step · 03 · Assets</span>
|
||||
<h2 className="mt-1.5 text-sm font-semibold text-white">选中主方案后展开资产清单</h2>
|
||||
<p className="mt-1 text-[11px] text-white/40">
|
||||
在上方九宫格选中一个方案,这里会展开 {totalImageSlots} 个图片位 + {TEXT_TEMPLATES.length} 个文案位 + {VIDEO_TEMPLATES.length} 个视频任务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -422,79 +425,103 @@ export default function PackPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const generatedTotal = packs.reduce((s, p) => s + p.assets.length, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Step 03 Header */}
|
||||
<section className="card p-6 space-y-5">
|
||||
<div className="space-y-4">
|
||||
{/* Step 03 header card */}
|
||||
<section className="card p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="section-eyebrow">Step · 03 · Lock & Generate</span>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">角色锁定 & 资产清单</h2>
|
||||
<p className="mt-1 text-[11px] text-white/45 leading-relaxed max-w-[480px]">
|
||||
以第一个选中图作为主方案。锁定角色设定后,下方按类型分组展示所有预设资产——每项都已固化标题、尺寸、解释和 Prompt,直接选哪一包就生成哪一包。
|
||||
<h2 className="mt-1.5 text-base font-semibold text-white">角色锁定 & 资产清单</h2>
|
||||
<p className="mt-0.5 text-[11px] text-white/40 max-w-[440px]">
|
||||
锁定角色设定后,下方四类图片包 + 文案包 + 视频任务已固化 Prompt,随时生成。
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-20 h-20 rounded-2xl overflow-hidden ring-1 ring-white/15 shrink-0 shadow-glow-violet">
|
||||
<img src={primaryImage.url} alt="selected source" className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
||||
<div className="absolute bottom-1 left-1 right-1 text-[9px] font-semibold text-white/90 uppercase tracking-wider truncate">主方案</div>
|
||||
{/* primary image + stats */}
|
||||
<div className="flex items-center gap-4 shrink-0">
|
||||
<div className="text-right text-[11px] text-white/40 space-y-0.5">
|
||||
<div className="font-mono text-white/70">{generatedTotal} <span className="text-white/30">/ {totalImageSlots} 张</span></div>
|
||||
<div className="text-[10px]">图片位</div>
|
||||
</div>
|
||||
<div className="relative w-16 h-16 rounded-2xl overflow-hidden ring-1 ring-white/15 shadow-glow-violet shrink-0">
|
||||
<img src={primaryImage.url} alt="selected" className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||
<div className="absolute bottom-1 inset-x-0 text-center text-[8px] font-semibold text-white/80 uppercase tracking-wider">主方案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* action buttons */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => onLockCharacter(primaryImage)}
|
||||
disabled={characterLoading || !!loadingKind || allLoading}
|
||||
className="btn btn-glass text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="btn btn-glass text-xs disabled:opacity-40"
|
||||
>
|
||||
{characterLoading ? (
|
||||
<>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
||||
</svg>
|
||||
锁定中
|
||||
</>
|
||||
) : session.characterSpec ? '刷新角色设定' : '锁定角色设定'}
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<rect x="5" y="11" width="14" height="10" rx="2" />
|
||||
<path d="M8 11V7a4 4 0 0 1 8 0v4" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
{characterLoading ? '锁定中' : session.characterSpec ? '刷新角色设定' : '锁定角色设定'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onGenerateAll(primaryImage)}
|
||||
disabled={allLoading || !!loadingKind || characterLoading}
|
||||
className="btn btn-primary text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="btn btn-primary text-xs disabled:opacity-40"
|
||||
>
|
||||
{allLoading ? (
|
||||
<>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
|
||||
</svg>
|
||||
全量生成中
|
||||
生成中
|
||||
</>
|
||||
) : '✨ 一键三包 · ' + (PACK_TEMPLATES.patent.length + PACK_TEMPLATES.production.length + PACK_TEMPLATES.marketing.length) + ' 张'}
|
||||
) : (
|
||||
`一键全包 · ${totalImageSlots} 张`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SlotOverview />
|
||||
|
||||
{/* CharacterSpec */}
|
||||
{session.characterSpec && (
|
||||
<div className="card-2 p-4">
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{session.characterSpec.name}</div>
|
||||
<div className="text-[11px] text-white/55 mt-1 line-clamp-2 max-w-[480px]">{session.characterSpec.oneLiner}</div>
|
||||
<div className="card-2 p-3.5">
|
||||
<div className="flex items-center justify-between gap-3 mb-2.5">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[13px] font-semibold text-white truncate">{session.characterSpec.name}</div>
|
||||
<div className="text-[11px] text-white/50 mt-0.5 line-clamp-1">{session.characterSpec.oneLiner}</div>
|
||||
</div>
|
||||
<span className="chip chip-violet shrink-0">CharacterSpec · v1</span>
|
||||
<span className="chip chip-violet text-[10px] shrink-0">CharacterSpec</span>
|
||||
</div>
|
||||
<div className="divider-line mb-3" />
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]">
|
||||
<div className="flex gap-2"><span className="text-white/40 w-12">形态</span><span className="text-white/85">{session.characterSpec.speciesShape}</span></div>
|
||||
<div className="flex gap-2"><span className="text-white/40 w-12">比例</span><span className="text-white/85">{session.characterSpec.bodyRatio}</span></div>
|
||||
<div className="flex gap-2"><span className="text-white/40 w-12">配色</span><span className="text-white/85">{session.characterSpec.colorPalette.join('、')}</span></div>
|
||||
<div className="flex gap-2"><span className="text-white/40 w-12">材料</span><span className="text-white/85">{session.characterSpec.materials.join('、')}</span></div>
|
||||
<div className="divider-line mb-2.5" />
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[11px]">
|
||||
{[
|
||||
['形态', session.characterSpec.speciesShape],
|
||||
['比例', session.characterSpec.bodyRatio],
|
||||
['配色', session.characterSpec.colorPalette.join('、')],
|
||||
['材料', session.characterSpec.materials.join('、')],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="flex gap-2 min-w-0">
|
||||
<span className="text-white/35 w-10 shrink-0">{label}</span>
|
||||
<span className="text-white/75 truncate">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section nav */}
|
||||
<SectionNav active={activeNav} onChange={setActiveNav} />
|
||||
</section>
|
||||
|
||||
{/* Pack Sections */}
|
||||
{/* Pack sections */}
|
||||
{PACK_ORDER.map(kind => {
|
||||
const pack = packs.find(p => p.kind === kind && p.sourceImageId === primaryImage.id);
|
||||
return (
|
||||
@@ -510,14 +537,9 @@ export default function PackPanel({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Text + Video */}
|
||||
<TextTemplateSection />
|
||||
|
||||
{/* Video Section */}
|
||||
<VideoSection
|
||||
videoLoading={videoLoading}
|
||||
primaryImage={primaryImage}
|
||||
onGenerateVideo={onGenerateVideo}
|
||||
/>
|
||||
<VideoSection videoLoading={videoLoading} primaryImage={primaryImage} onGenerateVideo={onGenerateVideo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user