diff --git a/src/app/globals.css b/src/app/globals.css index b036fed..bcacb9c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -961,6 +961,54 @@ input, textarea { display: none; } +.gallery-rail-thumb { + box-shadow: 0 12px 32px -22px rgba(230, 245, 120, 0.65); +} + +.gallery-rail-thumb:hover, +.gallery-rail-thumb:focus-visible { + outline: none; + transform: translateX(-2px); + box-shadow: + 0 0 0 1px rgba(230, 245, 120, 0.45), + 0 18px 42px -24px rgba(230, 245, 120, 0.82); +} + +.gallery-rail-empty, +.gallery-empty-state { + display: grid; + min-height: 88px; + place-items: center; + border-radius: 8px; + border: 1px dashed rgba(255, 255, 255, 0.12); + background: rgba(0, 0, 0, 0.16); + color: rgba(255, 255, 255, 0.34); + font-size: 10px; + text-align: center; +} + +.gallery-center-preview { + position: fixed; + z-index: 96; + left: 50%; + top: 50%; + pointer-events: none; + transform: translate(-50%, -50%); + border-radius: 8px; + background: #fff; + padding: 10px; + box-shadow: + 0 34px 110px -28px rgba(0, 0, 0, 0.92), + 0 0 0 1px rgba(255, 255, 255, 0.22); +} + +.gallery-center-preview img { + display: block; + width: min(74vw, 960px); + height: 82vh; + object-fit: contain; +} + .gallery-backdrop { position: fixed; inset: 0; @@ -993,6 +1041,36 @@ input, textarea { transform: translateX(0); } +.gallery-drawer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 14px; + align-items: start; +} + +.gallery-drawer-card { + min-width: 0; + border-radius: 8px; + background: rgba(255, 255, 255, 0.045); + padding: 10px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.gallery-drawer-card__image { + display: grid; + place-items: center; + overflow: hidden; + border-radius: 8px; + background: #fff; +} + +.gallery-drawer-card__image img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + .result-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); diff --git a/src/app/page.tsx b/src/app/page.tsx index ee4e55a..ae947e5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -109,11 +109,13 @@ function ProjectPackOverview({ primaryImage, loadingKind, onGeneratePack, + onOpenPack, }: { session: GenSession; primaryImage: GenImage | null; loadingKind: PackKind | null; onGeneratePack: (image: GenImage, kind: PackKind) => void; + onOpenPack: (kind: PackKind) => void; }) { if (!primaryImage) return null; const sourceImage = primaryImage; @@ -175,7 +177,7 @@ function ProjectPackOverview({ type="button" className="project-pack-jump" title="查看明细" - onClick={() => document.getElementById(`pack-${kind}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })} + onClick={() => onOpenPack(kind)} > @@ -196,12 +198,14 @@ function ProjectBrief({ characterLoading, onGeneratePack, onLockCharacter, + onOpenPack, }: { session: GenSession; loadingKind: PackKind | null; characterLoading: boolean; onGeneratePack: (image: GenImage, kind: PackKind) => void; onLockCharacter: (image: GenImage) => void; + onOpenPack: (kind: PackKind) => void; }) { const selectedImages = session.images.filter(image => image.status === 'selected'); const primaryImage = selectedImages[0] ?? session.images[0] ?? null; @@ -274,6 +278,7 @@ function ProjectBrief({ primaryImage={primaryImage} loadingKind={loadingKind} onGeneratePack={onGeneratePack} + onOpenPack={onOpenPack} /> {session.characterSpec ? ( @@ -335,6 +340,7 @@ export default function Home() { const [uploadLoading, setUploadLoading] = useState(false); const [provider, setProvider] = useState('?'); const [sidebarOpen, setSidebarOpen] = useState(true); + const [activeAssetPanel, setActiveAssetPanel] = useState('pack-patent'); const refreshSessions = useCallback(async () => { const r = await fetch('/api/sessions'); @@ -344,6 +350,7 @@ export default function Home() { }, []); useEffect(() => { refreshSessions(); }, [refreshSessions]); + useEffect(() => { setActiveAssetPanel('pack-patent'); }, [current?.id]); async function handleGenerate(opts: { prompt: string; refImages: string[]; count: number; style?: string }) { setLoading(true); @@ -634,16 +641,23 @@ export default function Home() { characterLoading={characterLoading} onGeneratePack={handleGeneratePack} onLockCharacter={handleLockCharacter} + onOpenPack={kind => setActiveAssetPanel(`pack-${kind}`)} />
- + )} diff --git a/src/components/PackPanel.tsx b/src/components/PackPanel.tsx index 76d30e3..131b0b8 100644 --- a/src/components/PackPanel.tsx +++ b/src/components/PackPanel.tsx @@ -513,16 +513,19 @@ function packPanelState(packs: AssetPack[], kind: PackKind, primaryImageId: stri export default function PackPanel({ session, + activeNav, + onActiveNavChange, videoLoading, onRegenerateAsset, onGenerateVideo, }: { session: GenSession; + activeNav: string; + onActiveNavChange: (id: string) => void; videoLoading: boolean; onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise; onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => 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 ?? []; @@ -563,7 +566,7 @@ export default function PackPanel({
查看与单张重做 - +
diff --git a/src/components/ProjectGalleryDrawer.tsx b/src/components/ProjectGalleryDrawer.tsx index ee4e9af..f073c56 100644 --- a/src/components/ProjectGalleryDrawer.tsx +++ b/src/components/ProjectGalleryDrawer.tsx @@ -2,24 +2,103 @@ import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -import type { GenSession } from '@/lib/types'; +import { PACK_LABELS, PACK_ORDER, PACK_TEMPLATES } from '@/lib/templates'; +import type { GenSession, PackKind } from '@/lib/types'; import ResultGrid from './ResultGrid'; export type ProjectGalleryDrawerProps = { session: GenSession; + activeNav: string; onAction: (imageId: string, action: 'select' | 'reject' | 'reset') => void; }; -function statusClass(status: string) { +type GalleryItem = { + id: string; + url: string; + title: string; + description?: string; + status?: string; + aspectRatio?: string; +}; + +type GalleryPanel = { + mode: 'pack' | 'empty' | 'project'; + label: string; + description: string; + total: number; + images: GalleryItem[]; +}; + +function statusClass(status?: string) { if (status === 'selected') return 'ring-[#e6f578]/80 shadow-[0_0_24px_-12px_rgba(230,245,120,0.9)]'; if (status === 'rejected') return 'opacity-45 grayscale ring-white/10'; return 'ring-white/12 hover:ring-white/28'; } -export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalleryDrawerProps) { +function aspectCss(aspectRatio?: string) { + if (!aspectRatio) return '1 / 1'; + if (aspectRatio === 'long') return '1 / 3'; + return aspectRatio.replace(':', ' / '); +} + +function activeKindFromNav(activeNav: string): PackKind | null { + const kind = PACK_ORDER.find(item => activeNav === `pack-${item}`); + return kind ?? null; +} + +function galleryForPanel(session: GenSession, activeNav: string): GalleryPanel { + const kind = activeKindFromNav(activeNav); + const selectedImages = session.images.filter(image => image.status === 'selected'); + const primaryImage = selectedImages[0] ?? session.images[0] ?? null; + + if (kind) { + const pack = session.packs?.find(item => item.kind === kind && (!primaryImage || item.sourceImageId === primaryImage.id)) + ?? session.packs?.find(item => item.kind === kind); + const images: GalleryItem[] = pack?.assets.map(asset => ({ + id: asset.id, + url: asset.url, + title: asset.title, + description: asset.description, + status: asset.status, + aspectRatio: asset.aspectRatio, + })) ?? []; + return { + mode: 'pack' as const, + label: PACK_LABELS[kind], + description: `${PACK_LABELS[kind]}当前区块图片`, + total: PACK_TEMPLATES[kind].length, + images, + }; + } + + if (activeNav === 'pack-text') { + return { mode: 'empty' as const, label: '文字', description: '文字区块暂无图片素材', total: 0, images: [] as GalleryItem[] }; + } + if (activeNav === 'pack-video') { + return { mode: 'empty' as const, label: '视频', description: '视频区块暂无图片素材', total: 0, images: [] as GalleryItem[] }; + } + + return { + mode: 'project', + label: '项目', + description: session.characterSpec?.name || session.prompt || '当前项目', + total: session.images.length, + images: session.images.map((image, index) => ({ + id: image.id, + url: image.url, + title: `主图库 ${index + 1}`, + description: undefined, + status: image.status, + aspectRatio: '1:1', + })), + }; +} + +export default function ProjectGalleryDrawer({ session, activeNav, onAction }: ProjectGalleryDrawerProps) { const [open, setOpen] = useState(false); const [mounted, setMounted] = useState(false); - const selectedCount = session.images.filter(image => image.status === 'selected').length; + const [preview, setPreview] = useState(null); + const gallery = galleryForPanel(session, activeNav); useEffect(() => { setMounted(true); @@ -41,9 +120,9 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
Project Gallery -

项目绘画图库

+

{gallery.label}图库

- {session.characterSpec?.name || session.prompt || '当前项目'} + {gallery.description}

- {open && } + {open && gallery.mode === 'project' ? ( + + ) : ( +
+ {gallery.images.map((image, index) => ( +
+
+ {image.title} +
+
+
+ {image.title} + {index + 1} +
+ {image.description && ( +

{image.description}

+ )} +
+
+ ))} + {!gallery.images.length && ( +
当前区块还没有可显示的图片。
+ )} +
+ )}
); + const centerPreview = preview ? ( + + ) : null; + return ( <> {mounted ? createPortal(drawer, document.body) : null} + {mounted && centerPreview ? createPortal(centerPreview, document.body) : null} ); }