fix: move project gallery to right drawer

This commit is contained in:
2026-05-20 13:45:27 +08:00
parent afe5e8bfdf
commit 4d047b0939
4 changed files with 199 additions and 34 deletions

View File

@@ -0,0 +1,117 @@
'use client';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import type { GenSession } from '@/lib/types';
import ResultGrid from './ResultGrid';
export type ProjectGalleryDrawerProps = {
session: GenSession;
onAction: (imageId: string, action: 'select' | 'reject' | 'reset') => void;
};
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) {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const selectedCount = session.images.filter(image => image.status === 'selected').length;
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!open) return;
function handleKey(event: KeyboardEvent) {
if (event.key === 'Escape') setOpen(false);
}
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [open]);
const drawer = (
<>
<div className={`gallery-backdrop ${open ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} onClick={() => setOpen(false)} />
<aside className={`gallery-drawer ${open ? 'is-open' : ''}`} aria-hidden={!open}>
<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>
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-white/42">
{session.characterSpec?.name || session.prompt || '当前项目'}
</p>
</div>
<button
onClick={() => setOpen(false)}
tabIndex={open ? 0 : -1}
className="grid h-10 w-10 shrink-0 cursor-pointer place-items-center rounded-[8px] bg-white/[0.06] text-white/60 ring-1 ring-white/10 transition-colors hover:bg-white/[0.10] hover:text-white"
title="关闭图库"
aria-label="关闭图库"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-5">
{open && <ResultGrid images={session.images} onAction={onAction} />}
</div>
</aside>
</>
);
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="打开项目图库"
>
<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" />
<path d="m7 14 3-3 3 3 2-2 2 2" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="8" cy="9" r="1.2" fill="currentColor" stroke="none" />
</svg>
<span className="mt-0.5 text-[9px] font-semibold"></span>
</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>
</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) => (
<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}`}
>
<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">
{index + 1}
</span>
{image.status === 'selected' && (
<span className="absolute right-1 top-1 grid h-5 w-5 place-items-center rounded-full bg-[#e6f578] text-[10px] font-bold text-[#081006]"></span>
)}
{image.status === 'rejected' && (
<span className="absolute right-1 top-1 grid h-5 w-5 place-items-center rounded-full bg-black/65 text-[10px] font-bold text-white">×</span>
)}
</button>
))}
</div>
</aside>
{mounted ? createPortal(drawer, document.body) : null}
</>
);
}