@@ -1,39 +1,293 @@
'use client' ;
import type { GenImage , GenSession , PackKind } from '@/lib/types ' ;
import { PACK_LABELS , PACK_ORDER , VIDEO_TEMPLATES } from '@/lib/templat es' ;
import { useState } from 'react ' ;
import type { AssetPack , AssetTemplate , GenImage , GenSession , PackKind , ToyAsset } from '@/lib/typ es' ;
import { PACK_LABELS , PACK_ORDER , PACK_TEMPLATES , VIDEO_TEMPLATES } from '@/lib/templates' ;
const PACK_DESCRIPTIONS : Record < PackKind , string > = {
patent : '六面视图、45° 立体图和 局部放大,用于 外观专利素材整理。 ' ,
production : '尺寸、材料、颜色、拆件和 包装结构,用于工厂报价与打样沟通。 ' ,
marketing : '白底商品图、场景图、细节图和 社媒图,用于 新品宣发。 ' ,
patent : '六面视图、45° 立体图、 局部放大—— 外观专利素材' ,
production : '尺寸、材料、颜色、拆件、 包装——工厂报价 / 打样 ' ,
marketing : '白底商品图、场景图、细节图、 社媒图—— 新品宣发' ,
} ;
const PACK_ACCENT : Record < PackKind , { ring : string ; chip : string ; dot : string ; bar : string } > = {
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' ,
} ,
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 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' ,
} ;
function manifestUrl ( sessionId : string , kind : PackKind , version : string ) {
return ` /api/export/ ${ sessionId } _ ${ kind } _ ${ version } _manifest.json ` ;
}
function AssetRow ( {
template ,
asset ,
accent ,
} : {
template : AssetTemplate ;
asset : ToyAsset | undefined ;
accent : typeof PACK_ACCENT [ PackKind ] ;
} ) {
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]" >
{ 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 >
) }
{ 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 >
) }
< / 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 >
) }
< / div >
< p className = "text-[11px] text-white/55 leading-relaxed line-clamp-2" > { 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"
>
< svg width = "10" height = "10" 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' }
< / 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" >
{ 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' } ` } >
{ ready ? 'Ready' : '待生成' }
< / div >
< / div >
< / div >
) ;
}
function PackSection ( {
kind ,
session ,
primaryImage ,
pack ,
isLoading ,
onGenerate ,
} : {
kind : PackKind ;
session : GenSession ;
primaryImage : GenImage ;
pack : ReturnType < NonNullable < GenSession [ 'packs' ] > [ number ] > extends never ? never : GenSession [ 'packs' ] extends Array < infer P > ? P : never ;
isLoading : boolean ;
onGenerate : ( ) = > void ;
} ) {
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 ;
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 === 'production' ? 'F' : 'M' }
< / 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 >
< / div >
< p className = "text-[11px] text-white/45 mt-0.5" > { PACK_DESCRIPTIONS [ kind ] } < / p >
< / div >
< / div >
< 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]"
>
< 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 ` }
>
{ 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 } 张 ` }
< / 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 >
< / 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 ,
} : {
videoLoading : boolean ;
primaryImage : GenImage ;
onGenerateVideo : ( image : GenImage , promptTemplate : string ) = > void ;
} ) {
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 >
< / div >
< / div >
< span className = "chip chip-violet" > Seedance < / span >
< / 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" >
1080 p
< / 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-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"
>
{ videoLoading ? '提交中' : '提交' }
< / button >
< / div >
< / div >
) ;
} ) }
< / div >
< / section >
) ;
}
export default function PackPanel ( {
session ,
loadingKind ,
@@ -66,9 +320,9 @@ export default function PackPanel({
< 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 >
< span className = "section-eyebrow" > Step · 03 · Lock Character < / span >
< h2 className = "mt-2 text-sm font-semibold text-white" > 下 一 步 素 材 包 < / h2 >
< h2 className = "mt-2 text-sm font-semibold text-white" > 下 一 步 : 资 产 清 单 < / h2 >
< p className = "mt-1 text-[11px] text-white/45 leading-relaxed" >
先 在 上 方 九 宫 格 选 中 一 个 主 方 案 , 再 生 成 专 利 、 生 产 、 宣 发 模 板 包 , 以 及 Seedance 视 频 。
先 在 上 方 九 宫 格 选 中 一 个 主 方 案 , 下 面 会 展 开 35 项 预 设 资 产 ( 专 利 / 生 产 / 宣 发 / 视 频 ) 。
< / p >
< / div >
< / div >
@@ -77,184 +331,97 @@ export default function PackPanel({
}
return (
< section className = "card p-6 space-y-6" >
< div className = "flex items-start justify-between gap-4" >
< div >
< span className = "section-eyebrow" > Step · 03 · Lock Character < / 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-[440px]" >
以 第 一 个 选 中 图 作 为 主 方 案 。 先 锁 定 角 色 设 定 , 再 全 量 生 成 三 类 素 材 包 , 所 有 图 引 用 同 一 份 CharacterSpec 保 持 一 致 性 。
< / 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 >
< / div >
< / div >
< div className = "flex flex-wrap gap-2" >
< button
onClick = { ( ) = > onLockCharacter ( primaryImage ) }
disabled = { characterLoading || ! ! loadingKind || allLoading }
className = "btn btn-glass text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
{ 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 ? '刷新角色设定' : '锁定角色设定' }
< / button >
< button
onClick = { ( ) = > onGenerateAll ( primaryImage ) }
disabled = { allLoading || ! ! loadingKind || characterLoading }
className = "btn btn-primary text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
{ allLoading ? '全量生成中' : '✨ 一键三包' }
< / button >
< / div >
{ 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-[440px]" > { session . characterSpec . oneLiner } < / div >
< / div >
< span className = "chip chip-violet shrink-0" > CharacterSpec · v1 < / 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 >
< / div >
) }
< div className = "grid grid-cols-3 gap-4" >
{ PACK_ORDER . map ( kind = > {
const pack = packs . find ( item = > item . kind === kind && item . sourceImageId === primaryImage . id ) ;
const isLoading = loadingKind === kind ;
const accent = PACK_ACCENT [ kind ] ;
return (
< div key = { kind } className = { ` relative rounded-2xl bg-white/[0.03] border border-white/[0.08] backdrop-blur-xl p-4 space-y-3 ring-1 ${ accent . ring } ` } >
< div className = { ` absolute top-0 left-4 right-4 h-px bg-gradient-to-r ${ accent . bar } opacity-60 ` } / >
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-2" >
< div className = { ` w-1.5 h-1.5 rounded-full ${ accent . dot } ` } / >
< div className = "text-sm font-semibold text-white" > { PACK_LABELS [ kind ] } < / div >
< / div >
{ pack && < span className = "text-[10px] text-white/50 font-mono" > { pack . version } < / span > }
< / div >
< p className = "text-[11px] text-white/45 leading-relaxed" > { PACK_DESCRIPTIONS [ kind ] } < / p >
< button
onClick = { ( ) = > onGenerate ( primaryImage , kind ) }
disabled = { ! ! loadingKind }
className = { ` ${ pack ? 'btn btn-outline' : 'btn btn-primary' } w-full text-xs disabled:opacity-40 disabled:cursor-not-allowed ` }
>
{ 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 ? '重新生成' : ` 生成 ${ PACK_LABELS [ kind ] } ` }
< / button >
{ pack && (
< div className = "pt-1 space-y-1.5" >
< div className = "text-[10px] text-white/55" >
已 生 成 < span className = "font-semibold text-white" > { pack . assets . length } < / span > 张
< / div >
< 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"
>
< 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 >
< / div >
) }
< / div >
) ;
} ) }
< / div >
{ packs . length > 0 && (
< div className = "space-y-5" >
{ packs . map ( pack = > {
const accent = PACK_ACCENT [ pack . kind ] ;
return (
< div key = { pack . id } className = "card-2 p-4 space-y-3" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-2" >
< div className = { ` w-1.5 h-1.5 rounded-full ${ accent . dot } ` } / >
< h3 className = "text-xs font-semibold text-white" >
{ PACK_LABELS [ pack . kind ] } < span className = "text-white/40 font-normal" > · { pack . assets . length } 张 < / span >
< / h3 >
< / div >
< code className = "text-[10px] text-white/30 font-mono" > { pack . id } < / code >
< / div >
< div className = "grid grid-cols-5 gap-2" >
{ pack . assets . map ( asset = > (
< div key = { asset . id } className = "group relative rounded-xl overflow-hidden ring-1 ring-white/[0.08] bg-white/[0.02] hover:ring-white/20 transition-all" >
< div className = "aspect-square bg-white/[0.03] overflow-hidden" >
< img src = { asset . url } alt = { asset . title } className = "w-full h-full object-cover" / >
< / div >
< div className = "p-2" >
< div className = "text-[11px] font-medium text-white/90 truncate" > { asset . title } < / div >
< div className = "text-[10px] text-white/40 truncate" > { asset . view } < / div >
< / div >
{ asset . required && (
< span className = "absolute top-1.5 right-1.5 px-1.5 py-0.5 rounded-md text-[9px] font-semibold text-violet-200 bg-violet-500/25 border border-violet-400/30 backdrop-blur" >
必 备
< / span >
) }
< / div >
) ) }
< / div >
< / div >
) ;
} ) }
< / div >
) }
< div className = "card-2 p-4 space-y-3 ring-1 ring-fuchsia-400/20" >
< div className = "absolute top-0 left-4 right-4 h-px bg-gradient-to-r from-fuchsia-400 to-violet-400 opacity-60" / >
< div className = "flex items-center justify-between" >
< div className = "space-y-6" >
{ /* Step 03 Header */ }
< section className = "card p-6 space-y-5" >
< div className = "flex items-start justify-between gap-4" >
< div >
< div className = "flex items-c ent er gap-2" >
< div className = "w-1.5 h-1.5 rounded-full bg-fuchsia-400" / >
< div className = "text-sm font-semibold text-white" > Seedance 视 频 < / div >
< / div >
< p className = "text-[11px] text-white/45 mt-1" >
视 频 固 定 走 Seedance · 用 当 前 主 方 案 生 成 宣 发 / 展 示 短 片
< span className = "section-eyebrow" > Step · 03 · Lock & G enerate < / 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 , 直 接 选 哪 一 包 就 生 成 哪 一 包 。
< / p >
< / div >
< span className = "chip chip-violet" > 异 步 任 务 < / span >
< 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 >
< / div >
< / div >
< div className = "grid grid-cols-2 md:grid-cols-4 gap-2" >
{ VIDEO_TEMPLATES . map ( template = > (
< button
key = { templa te . id }
onClick = { ( ) = > onGenerateVideo ( primaryImage , template . promptTemplate ) }
disabled = { videoLoading }
className = "btn btn-outline text-[11px] px-3 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed flex-col h-auto items-start gap-0.5"
title = { template . description }
>
< span className = "text-white/90 font-medium text-left w-full" > { template . title } < / span >
< s pan className = "text-[9px] text-white/40 truncate w-full text-left normal-case font-normal" > { template . description } < / span >
< / button >
) ) }
< div className = "flex flex-wrap gap-2" >
< button
onClick = { ( ) = > onLockCharac ter ( primaryImage ) }
disabled = { characterLoading || ! ! loadingKind || allLoading }
className = "btn btn-glass text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
{ 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 ? '刷新角色设定' : '锁定角色设定' }
< / button >
< button
onClick = { ( ) = > onGenerateAll ( primaryImage ) }
disabled = { allLoading || ! ! loadingKind || characterLoading }
className = "btn btn-primary text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
{ 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 ) + ' 张' }
< / button >
< / div >
< / div >
< / section >
{ 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 >
< span className = "chip chip-violet shrink-0" > CharacterSpec · v1 < / 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 >
< / div >
) }
< / section >
{ /* Pack Sections */ }
{ PACK_ORDER . map ( kind = > {
const pack = packs . find ( p = > p . kind === kind && p . sourceImageId === primaryImage . id ) ;
return (
< PackSection
key = { kind }
kind = { kind }
session = { session }
primaryImage = { primaryImage }
pack = { pack as never }
isLoading = { loadingKind === kind }
onGenerate = { ( ) = > onGenerate ( primaryImage , kind ) }
/ >
) ;
} ) }
{ /* Video Section */ }
< VideoSection
videoLoading = { videoLoading }
primaryImage = { primaryImage }
onGenerateVideo = { onGenerateVideo }
/ >
< / div >
) ;
}