fix: switch asset tabs as single panel
This commit is contained in:
@@ -247,7 +247,7 @@ function DetailItem({ label, value }: { label: string; value: string | number })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Pack Section (collapsible) ───────────────── */
|
/* ── Pack Section ─────────────────────────────── */
|
||||||
function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAsset }: {
|
function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAsset }: {
|
||||||
kind: PackKind;
|
kind: PackKind;
|
||||||
pack: AssetPack | undefined;
|
pack: AssetPack | undefined;
|
||||||
@@ -256,7 +256,6 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
|||||||
stepIndex: number;
|
stepIndex: number;
|
||||||
onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise<void>;
|
onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(kind === 'patent');
|
|
||||||
const [detail, setDetail] = useState<AssetDetail | null>(null);
|
const [detail, setDetail] = useState<AssetDetail | null>(null);
|
||||||
const accent = PACK_ACCENT[kind];
|
const accent = PACK_ACCENT[kind];
|
||||||
const templates = PACK_TEMPLATES[kind];
|
const templates = PACK_TEMPLATES[kind];
|
||||||
@@ -279,15 +278,6 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 font-mono text-[10px] text-white/35">{generatedCount}/{total}</span>
|
<span className="shrink-0 font-mono text-[10px] text-white/35">{generatedCount}/{total}</span>
|
||||||
<button
|
|
||||||
onClick={() => setOpen(v => !v)}
|
|
||||||
title={open ? '收起' : '展开'}
|
|
||||||
className="w-7 h-7 shrink-0 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>
|
||||||
|
|
||||||
{locked && lockReason && (
|
{locked && lockReason && (
|
||||||
@@ -296,8 +286,6 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* asset list — collapsible */}
|
|
||||||
{open && (
|
|
||||||
<div className="border-t border-white/[0.05] px-4 pb-4">
|
<div className="border-t border-white/[0.05] px-4 pb-4">
|
||||||
<div className="asset-strip pt-3">
|
<div className="asset-strip pt-3">
|
||||||
{templates.map(template => {
|
{templates.map(template => {
|
||||||
@@ -313,15 +301,13 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<AssetDetailDrawer detail={detail} onClose={() => setDetail(null)} onRegenerate={onRegenerateAsset} />
|
<AssetDetailDrawer detail={detail} onClose={() => setDetail(null)} onRegenerate={onRegenerateAsset} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Text Template Section (collapsible) ──────── */
|
/* ── Text Template Section ────────────────────── */
|
||||||
function TextTemplateSection({ locked }: { locked: boolean }) {
|
function TextTemplateSection({ locked }: { locked: boolean }) {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -340,22 +326,9 @@ function TextTemplateSection({ locked }: { locked: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<span className="chip bg-[#8cb478]/15 text-[#cfe7a7] border-[#8cb478]/30 text-[10px]">GPT Text</span>
|
<span className="chip bg-[#8cb478]/15 text-[#cfe7a7] border-[#8cb478]/30 text-[10px]">GPT Text</span>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!locked) setOpen(v => !v);
|
|
||||||
}}
|
|
||||||
disabled={locked}
|
|
||||||
title={locked ? '四个图片包完成后解锁文案任务' : 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 disabled:cursor-not-allowed disabled:opacity-35"
|
|
||||||
>
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
||||||
{TEXT_TEMPLATES.map(template => {
|
{TEXT_TEMPLATES.map(template => {
|
||||||
const isOpen = showPromptId === template.id;
|
const isOpen = showPromptId === template.id;
|
||||||
@@ -397,19 +370,17 @@ function TextTemplateSection({ locked }: { locked: boolean }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Video Section (collapsible) ──────────────── */
|
/* ── Video Section ────────────────────────────── */
|
||||||
function VideoSection({ videoLoading, primaryImage, locked, onGenerateVideo }: {
|
function VideoSection({ videoLoading, primaryImage, locked, onGenerateVideo }: {
|
||||||
videoLoading: boolean;
|
videoLoading: boolean;
|
||||||
primaryImage: GenImage;
|
primaryImage: GenImage;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => void;
|
onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => void;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
const [showPromptId, setShowPromptId] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -428,22 +399,9 @@ function VideoSection({ videoLoading, primaryImage, locked, onGenerateVideo }: {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<span className="chip chip-violet text-[10px]">Seedance</span>
|
<span className="chip chip-violet text-[10px]">Seedance</span>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (!locked) setOpen(v => !v);
|
|
||||||
}}
|
|
||||||
disabled={locked}
|
|
||||||
title={locked ? '四个图片包完成后解锁视频任务' : 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 disabled:cursor-not-allowed disabled:opacity-35"
|
|
||||||
>
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
<div className="px-4 pb-4 border-t border-white/[0.05] space-y-2 pt-3">
|
||||||
{VIDEO_TEMPLATES.map(template => {
|
{VIDEO_TEMPLATES.map(template => {
|
||||||
const isOpen = showPromptId === template.id;
|
const isOpen = showPromptId === template.id;
|
||||||
@@ -488,7 +446,6 @@ function VideoSection({ videoLoading, primaryImage, locked, onGenerateVideo }: {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -509,10 +466,7 @@ function SectionNav({ active, onChange }: { active: string; onChange: (id: strin
|
|||||||
{NAV_ITEMS.map(item => (
|
{NAV_ITEMS.map(item => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => {
|
onClick={() => onChange(item.id)}
|
||||||
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 ${
|
className={`shrink-0 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||||
active === item.id
|
active === item.id
|
||||||
? `text-[#081006] bg-gradient-to-r ${item.accent} shadow-sm`
|
? `text-[#081006] bg-gradient-to-r ${item.accent} shadow-sm`
|
||||||
@@ -543,6 +497,20 @@ function previousPackLabel(kind: PackKind) {
|
|||||||
return PACK_LABELS[PACK_ORDER[index - 1]];
|
return PACK_LABELS[PACK_ORDER[index - 1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function packPanelState(packs: AssetPack[], kind: PackKind, primaryImageId: string, characterReady: boolean) {
|
||||||
|
const pack = packForKind(packs, kind, primaryImageId);
|
||||||
|
const index = PACK_ORDER.indexOf(kind);
|
||||||
|
const previousKind = index > 0 ? PACK_ORDER[index - 1] : null;
|
||||||
|
const previousComplete = !previousKind || isPackComplete(packForKind(packs, previousKind, primaryImageId), previousKind);
|
||||||
|
const locked = !characterReady || !previousComplete;
|
||||||
|
const lockReason = !characterReady
|
||||||
|
? '先锁定角色设定,然后从专利包开始。'
|
||||||
|
: previousComplete
|
||||||
|
? undefined
|
||||||
|
: `请先完成${previousPackLabel(kind)},再生成${PACK_LABELS[kind]}。`;
|
||||||
|
return { pack, index, locked, lockReason };
|
||||||
|
}
|
||||||
|
|
||||||
export default function PackPanel({
|
export default function PackPanel({
|
||||||
session,
|
session,
|
||||||
videoLoading,
|
videoLoading,
|
||||||
@@ -583,6 +551,7 @@ export default function PackPanel({
|
|||||||
|
|
||||||
const completedPackCount = PACK_ORDER.filter(kind => isPackComplete(packForKind(packs, kind, primaryImage.id), kind)).length;
|
const completedPackCount = PACK_ORDER.filter(kind => isPackComplete(packForKind(packs, kind, primaryImage.id), kind)).length;
|
||||||
const imagePacksComplete = completedPackCount === PACK_ORDER.length;
|
const imagePacksComplete = completedPackCount === PACK_ORDER.length;
|
||||||
|
const activePackKind = PACK_ORDER.find(kind => activeNav === `pack-${kind}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||||
@@ -597,23 +566,12 @@ export default function PackPanel({
|
|||||||
<SectionNav active={activeNav} onChange={setActiveNav} />
|
<SectionNav active={activeNav} onChange={setActiveNav} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="pack-scroll min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
<div className="pack-scroll min-h-0 flex-1 overflow-y-auto pr-1" key={activeNav}>
|
||||||
{/* Pack sections */}
|
{activePackKind ? (() => {
|
||||||
{PACK_ORDER.map(kind => {
|
const { pack, index, locked, lockReason } = packPanelState(packs, activePackKind, primaryImage.id, characterReady);
|
||||||
const pack = packForKind(packs, kind, primaryImage.id);
|
|
||||||
const index = PACK_ORDER.indexOf(kind);
|
|
||||||
const previousKind = index > 0 ? PACK_ORDER[index - 1] : null;
|
|
||||||
const previousComplete = !previousKind || isPackComplete(packForKind(packs, previousKind, primaryImage.id), previousKind);
|
|
||||||
const locked = !characterReady || !previousComplete;
|
|
||||||
const lockReason = !characterReady
|
|
||||||
? '先锁定角色设定,然后从专利包开始。'
|
|
||||||
: previousComplete
|
|
||||||
? undefined
|
|
||||||
: `请先完成${previousPackLabel(kind)},再生成${PACK_LABELS[kind]}。`;
|
|
||||||
return (
|
return (
|
||||||
<PackSection
|
<PackSection
|
||||||
key={kind}
|
kind={activePackKind}
|
||||||
kind={kind}
|
|
||||||
pack={pack}
|
pack={pack}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
lockReason={lockReason}
|
lockReason={lockReason}
|
||||||
@@ -621,23 +579,25 @@ export default function PackPanel({
|
|||||||
onRegenerateAsset={onRegenerateAsset}
|
onRegenerateAsset={onRegenerateAsset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})() : (
|
||||||
|
|
||||||
{/* Text + Video */}
|
|
||||||
<div className={imagePacksComplete ? '' : 'opacity-55'}>
|
<div className={imagePacksComplete ? '' : 'opacity-55'}>
|
||||||
{!imagePacksComplete && (
|
{!imagePacksComplete && (
|
||||||
<div className="mb-3 rounded-[8px] border border-white/[0.06] bg-black/20 px-3 py-2 text-[11px] text-white/38">
|
<div className="mb-3 rounded-[8px] border border-white/[0.06] bg-black/20 px-3 py-2 text-[11px] text-white/38">
|
||||||
文案和视频在四个图片包完成后解锁。
|
文案和视频在四个图片包完成后解锁。
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{activeNav === 'pack-text' ? (
|
||||||
<TextTemplateSection locked={!imagePacksComplete} />
|
<TextTemplateSection locked={!imagePacksComplete} />
|
||||||
|
) : (
|
||||||
<VideoSection
|
<VideoSection
|
||||||
videoLoading={videoLoading}
|
videoLoading={videoLoading}
|
||||||
primaryImage={primaryImage}
|
primaryImage={primaryImage}
|
||||||
locked={!imagePacksComplete}
|
locked={!imagePacksComplete}
|
||||||
onGenerateVideo={onGenerateVideo}
|
onGenerateVideo={onGenerateVideo}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user