From d3d9349e8ba5a23c38a140f61148c0c2e05cc749 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 20 May 2026 19:00:44 +0800 Subject: [PATCH] fix: show pack assets as horizontal detail strip --- src/app/globals.css | 187 +++++++++++++++++++++ src/components/PackPanel.tsx | 303 +++++++++++++++++++++++------------ 2 files changed, 389 insertions(+), 101 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 6de13b2..0cd04e3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -784,6 +784,181 @@ input, textarea { background: rgba(255, 255, 255, 0.16); } +.asset-strip { + display: flex; + gap: 12px; + overflow-x: auto; + overflow-y: visible; + padding-bottom: 4px; + scroll-snap-type: x proximity; + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.18) transparent; +} + +.asset-strip::-webkit-scrollbar { + height: 8px; +} + +.asset-strip::-webkit-scrollbar-thumb { + border-radius: 999px; + background: rgba(255,255,255,0.16); +} + +.asset-tile { + flex: 0 0 clamp(136px, 12vw, 172px); + scroll-snap-align: start; + border-radius: 8px; + background: rgba(255,255,255,0.035); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07); + padding: 10px; + text-align: left; + transition: background 160ms ease, box-shadow 160ms ease, transform 160ms ease; +} + +.asset-tile:hover { + background: rgba(255,255,255,0.065); + box-shadow: + inset 0 0 0 1px rgba(230,245,120,0.22), + 0 18px 50px -34px rgba(230,245,120,0.55); + transform: translateY(-1px); +} + +.asset-tile:focus-visible { + outline: 2px solid rgba(230,245,120,0.45); + outline-offset: 2px; +} + +.asset-tile__thumb { + position: relative; + display: grid; + height: 118px; + width: 100%; + place-items: center; + overflow: visible; + border-radius: 8px; + background: rgba(0,0,0,0.28); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08); +} + +.asset-tile__thumb > img { + max-height: 118px; +} + +.asset-tile__state { + position: absolute; + right: 6px; + top: 6px; + display: grid; + width: 22px; + height: 22px; + place-items: center; + border-radius: 7px; + background: rgba(0,0,0,0.62); + color: rgba(255,255,255,0.62); + font-size: 10px; + font-weight: 700; + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.10); +} + +.asset-tile__state--done { + background: rgba(230,245,120,0.14); + color: #e6f578; + box-shadow: inset 0 0 0 1px rgba(230,245,120,0.28); +} + +.asset-tile__body { + min-width: 0; + padding-top: 9px; +} + +.asset-detail-backdrop { + position: fixed; + inset: 0; + z-index: 92; + pointer-events: none; + background: rgba(0,0,0,0.44); + opacity: 0; + backdrop-filter: blur(6px); + transition: opacity 180ms ease; +} + +.asset-detail-backdrop.is-open { + pointer-events: auto; + opacity: 1; +} + +.asset-detail-drawer { + position: fixed; + z-index: 96; + top: 0; + right: 0; + display: flex; + width: min(780px, calc(100vw - 96px)); + height: 100vh; + flex-direction: column; + border-left: 1px solid rgba(255,255,255,0.14); + background: + linear-gradient(135deg, rgba(255,255,255,0.09), rgba(255,255,255,0.035)), + rgba(20,20,21,0.94); + box-shadow: -28px 0 90px -34px rgba(0,0,0,0.92); + backdrop-filter: blur(22px); + transform: translateX(100%); + transition: transform 220ms ease; +} + +.asset-detail-drawer.is-open { + transform: translateX(0); +} + +.asset-detail-image { + display: grid; + max-height: min(54vh, 520px); + place-items: center; + overflow: hidden; + border-radius: 8px; + background: + linear-gradient(135deg, rgba(255,255,255,0.98), rgba(238,242,230,0.94)); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.46); +} + +.asset-detail-image img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.asset-detail-block { + margin-top: 14px; + border-radius: 8px; + background: rgba(0,0,0,0.22); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07); + padding: 13px; +} + +.asset-detail-label { + margin-bottom: 8px; + color: rgba(255,255,255,0.42); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.asset-detail-code, +.asset-detail-pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 11px; + line-height: 1.6; + color: rgba(255,255,255,0.68); + overflow-wrap: anywhere; +} + +.asset-detail-pre { + max-height: 220px; + overflow-y: auto; + white-space: pre-wrap; +} + .gallery-thumb-list { scrollbar-width: none; } @@ -899,6 +1074,18 @@ input, textarea { display: none; } + .asset-tile { + flex-basis: 142px; + } + + .asset-detail-drawer { + width: calc(100vw - 28px); + } + + .asset-detail-image { + max-height: 42vh; + } + .session-workspace { height: auto; min-height: 0; diff --git a/src/components/PackPanel.tsx b/src/components/PackPanel.tsx index 850756f..2f93cd9 100644 --- a/src/components/PackPanel.tsx +++ b/src/components/PackPanel.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; import type { AssetPack, AssetTemplate, GenImage, GenSession, PackKind, ToyAsset } from '@/lib/types'; import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES, TEXT_TEMPLATES, VIDEO_TEMPLATES } from '@/lib/templates'; import { HoverImagePreview } from './HoverImagePreview'; @@ -34,121 +35,214 @@ function aspectCss(aspectRatio: AssetTemplate['aspectRatio'] | string | undefine return aspectRatio.replace(':', ' / '); } -/* ── Asset Row ────────────────────────────────── */ -function AssetRow({ template, asset, accent, onRegenerate }: { +type AssetDetail = { template: AssetTemplate; asset: ToyAsset | undefined; - accent: typeof PACK_ACCENT[PackKind]; - onRegenerate?: (assetId: string, userRefinement?: string) => Promise; +}; + +function formatDate(timestamp?: number) { + if (!timestamp) return '未生成'; + return new Date(timestamp).toLocaleString('zh-CN'); +} + +function AssetTile({ template, asset, onOpen }: { + template: AssetTemplate; + asset: ToyAsset | undefined; + onOpen: () => void; }) { - const [showPrompt, setShowPrompt] = useState(false); - const [showRedo, setShowRedo] = useState(false); + const ready = !!asset; + + return ( + + ); +} + +function AssetDetailDrawer({ detail, onClose, onRegenerate }: { + detail: AssetDetail | null; + onClose: () => void; + onRegenerate: (assetId: string, userRefinement?: string) => Promise; +}) { + const [mounted, setMounted] = useState(false); const [refinement, setRefinement] = useState(''); const [regenerating, setRegenerating] = useState(false); - const ready = !!asset; + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!detail) return; + setRefinement(''); + function handleKey(event: KeyboardEvent) { + if (event.key === 'Escape') onClose(); + } + window.addEventListener('keydown', handleKey); + return () => window.removeEventListener('keydown', handleKey); + }, [detail, onClose]); + async function handleRedo() { + const asset = detail?.asset; if (!asset || !onRegenerate || regenerating) return; const ok = window.confirm('重新生成这 1 张会再次调用图片模型并产生费用。确认继续?'); if (!ok) return; setRegenerating(true); try { await onRegenerate(asset.id, refinement); - setShowRedo(false); setRefinement(''); + onClose(); } finally { setRegenerating(false); } } + + if (!mounted) return null; + + const template = detail?.template; + const asset = detail?.asset; + const ready = !!asset; + const drawer = ( + <> +
+ + + ); + + return createPortal(drawer, document.body); +} + +function DetailItem({ label, value }: { label: string; value: string | number }) { return ( -
- {/* thumbnail */} -
- {ready ? ( - - ) : ( -
- {template.aspectRatio} -
- )} - {template.required && !ready && ( - - )} -
- - {/* info */} -
-
- {template.title} - {template.required && !ready && ( - 必备 - )} - {ready && } -
-

{template.description}

- - {showPrompt && ( -
-            {asset?.prompt ?? template.promptTemplate}
-          
- )} - {ready && asset.anchorImageUrl && ( -
- anchor: {asset.anchorAssetId ?? asset.anchorImageUrl} -
- )} -
- - {/* meta */} -
- {ASPECT_PX[template.aspectRatio]} -
- - {ready ? `L${asset!.derivationLevel}` : '待生成'} - - {ready && onRegenerate && ( - - )} -
-
- - {showRedo && ready && ( -
- setRefinement(event.target.value)} - placeholder="重做要求" - className="min-w-0 flex-1 rounded-[8px] bg-black/30 ring-1 ring-white/[0.08] px-2.5 py-2 text-[11px] text-white/80 outline-none focus:ring-[#e6f578]/40" - /> - -
- )} +
+
{label}
+
{value}
); } @@ -163,6 +257,7 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise; }) { const [open, setOpen] = useState(kind === 'patent'); + const [detail, setDetail] = useState(null); const accent = PACK_ACCENT[kind]; const templates = PACK_TEMPLATES[kind]; const generatedCount = pack?.assets.length ?? 0; @@ -203,17 +298,23 @@ function PackSection({ kind, pack, locked, lockReason, stepIndex, onRegenerateAs {/* asset list — collapsible */} {open && ( -
-
+
+
{templates.map(template => { const asset = pack?.assets.find(a => a.templateId === template.id); return ( - + setDetail({ template, asset })} + /> ); })}
)} + setDetail(null)} onRegenerate={onRegenerateAsset} /> ); }