fix: move project gallery to right drawer
This commit is contained in:
@@ -1503,6 +1503,27 @@
|
|||||||
"message": "auto-save 2026-05-20 09:54 (~3)",
|
"message": "auto-save 2026-05-20 09:54 (~3)",
|
||||||
"hash": "7ad323a",
|
"hash": "7ad323a",
|
||||||
"files_changed": 3
|
"files_changed": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-20T12:33:03+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-20 12:33 (+1, ~1)",
|
||||||
|
"hash": "a62f9b1",
|
||||||
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-20T12:38:29+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-20 12:38 (~3)",
|
||||||
|
"hash": "a25b740",
|
||||||
|
"files_changed": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-20T12:49:21+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-20 12:49 (~3)",
|
||||||
|
"hash": "a190800",
|
||||||
|
"files_changed": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,31 +361,63 @@ input, textarea {
|
|||||||
|
|
||||||
.session-split {
|
.session-split {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(360px, 430px);
|
grid-template-columns: minmax(0, 1fr) 74px;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-image-pane,
|
|
||||||
.pack-scroll {
|
.pack-scroll {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-image-pane::-webkit-scrollbar,
|
|
||||||
.pack-scroll::-webkit-scrollbar {
|
.pack-scroll::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-image-pane::-webkit-scrollbar-thumb,
|
|
||||||
.pack-scroll::-webkit-scrollbar-thumb {
|
.pack-scroll::-webkit-scrollbar-thumb {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.16);
|
background: rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-pack-pane {
|
.gallery-thumb-list {
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
scrollbar-width: none;
|
||||||
padding-left: 18px;
|
}
|
||||||
|
|
||||||
|
.gallery-thumb-list::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 70;
|
||||||
|
background: rgba(0, 0, 0, 0.42);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
transition: opacity 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-drawer {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 80;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: min(820px, calc(100vw - 96px));
|
||||||
|
display: flex;
|
||||||
|
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.92);
|
||||||
|
box-shadow: -28px 0 90px -34px rgba(0, 0, 0, 0.92);
|
||||||
|
backdrop-filter: blur(22px);
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 220ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-drawer.is-open {
|
||||||
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-grid {
|
.result-grid {
|
||||||
@@ -404,13 +436,9 @@ input, textarea {
|
|||||||
|
|
||||||
@media (min-width: 1760px) {
|
@media (min-width: 1760px) {
|
||||||
.session-split {
|
.session-split {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(400px, 480px);
|
grid-template-columns: minmax(0, 1fr) 84px;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-pack-pane {
|
|
||||||
padding-left: 22px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
@@ -424,16 +452,25 @@ input, textarea {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-image-pane,
|
.gallery-rail {
|
||||||
.session-pack-pane {
|
min-height: 96px;
|
||||||
overflow: visible;
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-pack-pane {
|
.gallery-thumb-list {
|
||||||
border-left: 0;
|
flex-direction: row;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
overflow-x: auto;
|
||||||
padding-left: 0;
|
overflow-y: hidden;
|
||||||
padding-top: 18px;
|
}
|
||||||
|
|
||||||
|
.gallery-rail button {
|
||||||
|
width: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-drawer {
|
||||||
|
width: min(100vw, 720px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import PromptPanel from '@/components/PromptPanel';
|
import PromptPanel from '@/components/PromptPanel';
|
||||||
import ResultGrid from '@/components/ResultGrid';
|
|
||||||
import Sidebar from '@/components/Sidebar';
|
import Sidebar from '@/components/Sidebar';
|
||||||
import PackPanel from '@/components/PackPanel';
|
import PackPanel from '@/components/PackPanel';
|
||||||
|
import ProjectGalleryDrawer from '@/components/ProjectGalleryDrawer';
|
||||||
import { OasisCanvas } from '@/components/login/OasisCanvas';
|
import { OasisCanvas } from '@/components/login/OasisCanvas';
|
||||||
import type {
|
import type {
|
||||||
GenImage,
|
GenImage,
|
||||||
@@ -311,14 +311,6 @@ export default function Home() {
|
|||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{current && (
|
{current && (
|
||||||
<>
|
<>
|
||||||
<a
|
|
||||||
href={`/api/gallery/${encodeURIComponent(current.id)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="chip chip-neutral hover:border-[#e6f578]/40 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
图库
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href={`/api/audit/${encodeURIComponent(current.id)}`}
|
href={`/api/audit/${encodeURIComponent(current.id)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -356,8 +348,8 @@ export default function Home() {
|
|||||||
<section className="dashboard-workbench session-workspace flex flex-col gap-5 p-5">
|
<section className="dashboard-workbench session-workspace flex flex-col gap-5 p-5">
|
||||||
<div className="flex shrink-0 items-end justify-between gap-4">
|
<div className="flex shrink-0 items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<span className="section-eyebrow">Step · 02 · Quick Screen</span>
|
<span className="section-eyebrow">Project Workspace</span>
|
||||||
<h2 className="mt-2 text-lg font-semibold text-white">本次生成</h2>
|
<h2 className="mt-2 text-lg font-semibold text-white">项目工作台</h2>
|
||||||
<p className="text-xs text-white/40 mt-1">
|
<p className="text-xs text-white/40 mt-1">
|
||||||
{new Date(current.createdAt).toLocaleString('zh-CN')}
|
{new Date(current.createdAt).toLocaleString('zh-CN')}
|
||||||
</p>
|
</p>
|
||||||
@@ -365,9 +357,6 @@ export default function Home() {
|
|||||||
<code className="max-w-[220px] truncate text-[11px] text-white/30 font-mono">{current.id}</code>
|
<code className="max-w-[220px] truncate text-[11px] text-white/30 font-mono">{current.id}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="session-split min-h-0 flex-1">
|
<div className="session-split min-h-0 flex-1">
|
||||||
<div className="session-image-pane min-h-0 overflow-y-auto pr-1">
|
|
||||||
<ResultGrid images={current.images} onAction={handleAction} />
|
|
||||||
</div>
|
|
||||||
<aside className="session-pack-pane min-h-0 overflow-hidden">
|
<aside className="session-pack-pane min-h-0 overflow-hidden">
|
||||||
<PackPanel
|
<PackPanel
|
||||||
session={current}
|
session={current}
|
||||||
@@ -382,6 +371,7 @@ export default function Home() {
|
|||||||
onGenerateVideo={handleGenerateVideo}
|
onGenerateVideo={handleGenerateVideo}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
<ProjectGalleryDrawer session={current} onAction={handleAction} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
117
src/components/ProjectGalleryDrawer.tsx
Normal file
117
src/components/ProjectGalleryDrawer.tsx
Normal 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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user