fix: sync side gallery with active asset panel
This commit is contained in:
@@ -961,6 +961,54 @@ input, textarea {
|
|||||||
display: none;
|
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 {
|
.gallery-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -993,6 +1041,36 @@ input, textarea {
|
|||||||
transform: translateX(0);
|
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 {
|
.result-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||||
|
|||||||
@@ -109,11 +109,13 @@ function ProjectPackOverview({
|
|||||||
primaryImage,
|
primaryImage,
|
||||||
loadingKind,
|
loadingKind,
|
||||||
onGeneratePack,
|
onGeneratePack,
|
||||||
|
onOpenPack,
|
||||||
}: {
|
}: {
|
||||||
session: GenSession;
|
session: GenSession;
|
||||||
primaryImage: GenImage | null;
|
primaryImage: GenImage | null;
|
||||||
loadingKind: PackKind | null;
|
loadingKind: PackKind | null;
|
||||||
onGeneratePack: (image: GenImage, kind: PackKind) => void;
|
onGeneratePack: (image: GenImage, kind: PackKind) => void;
|
||||||
|
onOpenPack: (kind: PackKind) => void;
|
||||||
}) {
|
}) {
|
||||||
if (!primaryImage) return null;
|
if (!primaryImage) return null;
|
||||||
const sourceImage = primaryImage;
|
const sourceImage = primaryImage;
|
||||||
@@ -175,7 +177,7 @@ function ProjectPackOverview({
|
|||||||
type="button"
|
type="button"
|
||||||
className="project-pack-jump"
|
className="project-pack-jump"
|
||||||
title="查看明细"
|
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">
|
<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" />
|
<path d="M9 6l6 6-6 6" strokeLinecap="round" />
|
||||||
@@ -196,12 +198,14 @@ function ProjectBrief({
|
|||||||
characterLoading,
|
characterLoading,
|
||||||
onGeneratePack,
|
onGeneratePack,
|
||||||
onLockCharacter,
|
onLockCharacter,
|
||||||
|
onOpenPack,
|
||||||
}: {
|
}: {
|
||||||
session: GenSession;
|
session: GenSession;
|
||||||
loadingKind: PackKind | null;
|
loadingKind: PackKind | null;
|
||||||
characterLoading: boolean;
|
characterLoading: boolean;
|
||||||
onGeneratePack: (image: GenImage, kind: PackKind) => void;
|
onGeneratePack: (image: GenImage, kind: PackKind) => void;
|
||||||
onLockCharacter: (image: GenImage) => void;
|
onLockCharacter: (image: GenImage) => void;
|
||||||
|
onOpenPack: (kind: PackKind) => void;
|
||||||
}) {
|
}) {
|
||||||
const selectedImages = session.images.filter(image => image.status === 'selected');
|
const selectedImages = session.images.filter(image => image.status === 'selected');
|
||||||
const primaryImage = selectedImages[0] ?? session.images[0] ?? null;
|
const primaryImage = selectedImages[0] ?? session.images[0] ?? null;
|
||||||
@@ -274,6 +278,7 @@ function ProjectBrief({
|
|||||||
primaryImage={primaryImage}
|
primaryImage={primaryImage}
|
||||||
loadingKind={loadingKind}
|
loadingKind={loadingKind}
|
||||||
onGeneratePack={onGeneratePack}
|
onGeneratePack={onGeneratePack}
|
||||||
|
onOpenPack={onOpenPack}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{session.characterSpec ? (
|
{session.characterSpec ? (
|
||||||
@@ -335,6 +340,7 @@ export default function Home() {
|
|||||||
const [uploadLoading, setUploadLoading] = useState(false);
|
const [uploadLoading, setUploadLoading] = useState(false);
|
||||||
const [provider, setProvider] = useState<string>('?');
|
const [provider, setProvider] = useState<string>('?');
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
|
const [activeAssetPanel, setActiveAssetPanel] = useState('pack-patent');
|
||||||
|
|
||||||
const refreshSessions = useCallback(async () => {
|
const refreshSessions = useCallback(async () => {
|
||||||
const r = await fetch('/api/sessions');
|
const r = await fetch('/api/sessions');
|
||||||
@@ -344,6 +350,7 @@ export default function Home() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { refreshSessions(); }, [refreshSessions]);
|
useEffect(() => { refreshSessions(); }, [refreshSessions]);
|
||||||
|
useEffect(() => { setActiveAssetPanel('pack-patent'); }, [current?.id]);
|
||||||
|
|
||||||
async function handleGenerate(opts: { prompt: string; refImages: string[]; count: number; style?: string }) {
|
async function handleGenerate(opts: { prompt: string; refImages: string[]; count: number; style?: string }) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -634,16 +641,23 @@ export default function Home() {
|
|||||||
characterLoading={characterLoading}
|
characterLoading={characterLoading}
|
||||||
onGeneratePack={handleGeneratePack}
|
onGeneratePack={handleGeneratePack}
|
||||||
onLockCharacter={handleLockCharacter}
|
onLockCharacter={handleLockCharacter}
|
||||||
|
onOpenPack={kind => setActiveAssetPanel(`pack-${kind}`)}
|
||||||
/>
|
/>
|
||||||
<section className="project-production-panel">
|
<section className="project-production-panel">
|
||||||
<PackPanel
|
<PackPanel
|
||||||
session={current}
|
session={current}
|
||||||
|
activeNav={activeAssetPanel}
|
||||||
|
onActiveNavChange={setActiveAssetPanel}
|
||||||
videoLoading={videoLoading}
|
videoLoading={videoLoading}
|
||||||
onRegenerateAsset={handleRegenerateAsset}
|
onRegenerateAsset={handleRegenerateAsset}
|
||||||
onGenerateVideo={handleGenerateVideo}
|
onGenerateVideo={handleGenerateVideo}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<ProjectGalleryDrawer session={current} onAction={handleAction} />
|
<ProjectGalleryDrawer
|
||||||
|
session={current}
|
||||||
|
activeNav={activeAssetPanel}
|
||||||
|
onAction={handleAction}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -513,16 +513,19 @@ function packPanelState(packs: AssetPack[], kind: PackKind, primaryImageId: stri
|
|||||||
|
|
||||||
export default function PackPanel({
|
export default function PackPanel({
|
||||||
session,
|
session,
|
||||||
|
activeNav,
|
||||||
|
onActiveNavChange,
|
||||||
videoLoading,
|
videoLoading,
|
||||||
onRegenerateAsset,
|
onRegenerateAsset,
|
||||||
onGenerateVideo,
|
onGenerateVideo,
|
||||||
}: {
|
}: {
|
||||||
session: GenSession;
|
session: GenSession;
|
||||||
|
activeNav: string;
|
||||||
|
onActiveNavChange: (id: string) => void;
|
||||||
videoLoading: boolean;
|
videoLoading: boolean;
|
||||||
onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise<void>;
|
onRegenerateAsset: (assetId: string, userRefinement?: string) => Promise<void>;
|
||||||
onGenerateVideo: (image: GenImage, template: typeof VIDEO_TEMPLATES[number]) => 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 selectedImages = session.images.filter(image => image.status === 'selected');
|
||||||
const primaryImage = selectedImages[0] ?? null;
|
const primaryImage = selectedImages[0] ?? null;
|
||||||
const packs = session.packs ?? [];
|
const packs = session.packs ?? [];
|
||||||
@@ -563,7 +566,7 @@ export default function PackPanel({
|
|||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-[10px] text-white/35">查看与单张重做</span>
|
<span className="shrink-0 text-[10px] text-white/35">查看与单张重做</span>
|
||||||
</div>
|
</div>
|
||||||
<SectionNav active={activeNav} onChange={setActiveNav} />
|
<SectionNav active={activeNav} onChange={onActiveNavChange} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="pack-scroll min-h-0 flex-1 overflow-y-auto pr-1" key={activeNav}>
|
<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 { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
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';
|
import ResultGrid from './ResultGrid';
|
||||||
|
|
||||||
export type ProjectGalleryDrawerProps = {
|
export type ProjectGalleryDrawerProps = {
|
||||||
session: GenSession;
|
session: GenSession;
|
||||||
|
activeNav: string;
|
||||||
onAction: (imageId: string, action: 'select' | 'reject' | 'reset') => void;
|
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 === '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';
|
if (status === 'rejected') return 'opacity-45 grayscale ring-white/10';
|
||||||
return 'ring-white/12 hover:ring-white/28';
|
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 [open, setOpen] = useState(false);
|
||||||
const [mounted, setMounted] = 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(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
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="flex shrink-0 items-start justify-between gap-4 border-b border-white/10 p-5">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="section-eyebrow">Project Gallery</span>
|
<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">
|
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-white/42">
|
||||||
{session.characterSpec?.name || session.prompt || '当前项目'}
|
{gallery.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -59,20 +138,50 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto p-5">
|
<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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const centerPreview = preview ? (
|
||||||
|
<div className="gallery-center-preview" aria-hidden="true">
|
||||||
|
<img src={preview.url} alt="" />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
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">
|
<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
|
<button
|
||||||
onClick={() => setOpen(true)}
|
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]"
|
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="打开项目图库"
|
title={`打开${gallery.label}图库`}
|
||||||
aria-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">
|
<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" />
|
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||||
@@ -83,18 +192,23 @@ export default function ProjectGalleryDrawer({ session, onAction }: ProjectGalle
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="w-full rounded-[8px] bg-black/22 px-1.5 py-2 text-center text-[10px] text-white/42">
|
<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>
|
<b className="block text-[13px] text-[#e6f578]">{gallery.images.length}</b>
|
||||||
<span>/ {session.images.length}</span>
|
<span>/ {gallery.total}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gallery-thumb-list flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto pr-0.5">
|
<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
|
<button
|
||||||
key={image.id}
|
key={image.id}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className={`relative aspect-square w-full cursor-pointer overflow-hidden rounded-[8px] bg-white ring-1 transition-all ${statusClass(image.status)}`}
|
onMouseEnter={() => setPreview(image)}
|
||||||
title={`打开第 ${index + 1} 张`}
|
onMouseLeave={() => setPreview(null)}
|
||||||
aria-label={`打开第 ${index + 1} 张`}
|
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" />
|
<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">
|
<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>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{!gallery.images.length && (
|
||||||
|
<div className="gallery-rail-empty">暂无图片</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{mounted ? createPortal(drawer, document.body) : null}
|
{mounted ? createPortal(drawer, document.body) : null}
|
||||||
|
{mounted && centerPreview ? createPortal(centerPreview, document.body) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user