fix: sync side gallery with active asset panel
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M9 6l6 6-6 6" strokeLinecap="round" />
|
||||
@@ -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<string>('?');
|
||||
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}`)}
|
||||
/>
|
||||
<section className="project-production-panel">
|
||||
<PackPanel
|
||||
session={current}
|
||||
activeNav={activeAssetPanel}
|
||||
onActiveNavChange={setActiveAssetPanel}
|
||||
videoLoading={videoLoading}
|
||||
onRegenerateAsset={handleRegenerateAsset}
|
||||
onGenerateVideo={handleGenerateVideo}
|
||||
/>
|
||||
</section>
|
||||
<ProjectGalleryDrawer session={current} onAction={handleAction} />
|
||||
<ProjectGalleryDrawer
|
||||
session={current}
|
||||
activeNav={activeAssetPanel}
|
||||
onAction={handleAction}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -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<void>;
|
||||
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({
|
||||
</div>
|
||||
<span className="shrink-0 text-[10px] text-white/35">查看与单张重做</span>
|
||||
</div>
|
||||
<SectionNav active={activeNav} onChange={setActiveNav} />
|
||||
<SectionNav active={activeNav} onChange={onActiveNavChange} />
|
||||
</section>
|
||||
|
||||
<div className="pack-scroll min-h-0 flex-1 overflow-y-auto pr-1" key={activeNav}>
|
||||
|
||||
@@ -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<GalleryItem | null>(null);
|
||||
const gallery = galleryForPanel(session, activeNav);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -41,9 +120,9 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
||||
<div className="flex shrink-0 items-start justify-between gap-4 border-b border-white/10 p-5">
|
||||
<div className="min-w-0">
|
||||
<span className="section-eyebrow">Project Gallery</span>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">项目绘画图库</h2>
|
||||
<h2 className="mt-2 text-lg font-semibold text-white">{gallery.label}图库</h2>
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-white/42">
|
||||
{session.characterSpec?.name || session.prompt || '当前项目'}
|
||||
{gallery.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -59,20 +138,50 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-5">
|
||||
{open && <ResultGrid images={session.images} onAction={onAction} />}
|
||||
{open && gallery.mode === 'project' ? (
|
||||
<ResultGrid images={session.images} onAction={onAction} />
|
||||
) : (
|
||||
<div className="gallery-drawer-grid">
|
||||
{gallery.images.map((image, index) => (
|
||||
<div key={image.id} className="gallery-drawer-card">
|
||||
<div className="gallery-drawer-card__image" style={{ aspectRatio: aspectCss(image.aspectRatio) }}>
|
||||
<img src={image.url} alt={image.title} />
|
||||
</div>
|
||||
<div className="mt-3 min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-[12px] font-semibold text-white">{image.title}</span>
|
||||
<span className="shrink-0 font-mono text-[10px] text-[#dff5a8]">{index + 1}</span>
|
||||
</div>
|
||||
{image.description && (
|
||||
<p className="mt-1 line-clamp-2 text-[10px] leading-relaxed text-white/42">{image.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!gallery.images.length && (
|
||||
<div className="gallery-empty-state">当前区块还没有可显示的图片。</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
const centerPreview = preview ? (
|
||||
<div className="gallery-center-preview" aria-hidden="true">
|
||||
<img src={preview.url} alt="" />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="gallery-rail flex h-full min-h-0 flex-col items-center gap-3 overflow-hidden rounded-[8px] border border-white/10 bg-white/[0.045] p-2 backdrop-blur-xl">
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex h-12 w-full cursor-pointer flex-col items-center justify-center rounded-[8px] bg-[#e6f578] text-[#081006] transition-colors hover:bg-[#f0fb82]"
|
||||
title="打开项目图库"
|
||||
aria-label="打开项目图库"
|
||||
title={`打开${gallery.label}图库`}
|
||||
aria-label={`打开${gallery.label}图库`}
|
||||
>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
@@ -83,18 +192,23 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
||||
</button>
|
||||
|
||||
<div className="w-full rounded-[8px] bg-black/22 px-1.5 py-2 text-center text-[10px] text-white/42">
|
||||
<b className="block text-[13px] text-[#e6f578]">{selectedCount}</b>
|
||||
<span>/ {session.images.length}</span>
|
||||
<b className="block text-[13px] text-[#e6f578]">{gallery.images.length}</b>
|
||||
<span>/ {gallery.total}</span>
|
||||
</div>
|
||||
|
||||
<div className="gallery-thumb-list flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto pr-0.5">
|
||||
{session.images.map((image, index) => (
|
||||
{gallery.images.map((image, index) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => setOpen(true)}
|
||||
className={`relative aspect-square w-full cursor-pointer overflow-hidden rounded-[8px] bg-white ring-1 transition-all ${statusClass(image.status)}`}
|
||||
title={`打开第 ${index + 1} 张`}
|
||||
aria-label={`打开第 ${index + 1} 张`}
|
||||
onMouseEnter={() => setPreview(image)}
|
||||
onMouseLeave={() => setPreview(null)}
|
||||
onFocus={() => setPreview(image)}
|
||||
onBlur={() => setPreview(null)}
|
||||
className={`gallery-rail-thumb relative w-full cursor-pointer overflow-hidden rounded-[8px] bg-white ring-1 transition-all ${statusClass(image.status)}`}
|
||||
style={{ aspectRatio: aspectCss(image.aspectRatio) }}
|
||||
title={`查看${image.title}`}
|
||||
aria-label={`查看${image.title}`}
|
||||
>
|
||||
<img src={image.url} alt="" className="h-full w-full object-contain" />
|
||||
<span className="absolute left-1 top-1 rounded-[6px] bg-black/60 px-1.5 py-0.5 text-[9px] font-semibold text-white">
|
||||
@@ -108,10 +222,14 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{!gallery.images.length && (
|
||||
<div className="gallery-rail-empty">暂无图片</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{mounted ? createPortal(drawer, document.body) : null}
|
||||
{mounted && centerPreview ? createPortal(centerPreview, document.body) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user