Refine gallery border flow thickness

This commit is contained in:
FengLee
2026-05-13 23:46:46 +08:00
parent 5c5cb6c907
commit 8c7dbea597
2 changed files with 112 additions and 155 deletions

View File

@@ -707,114 +707,113 @@ export default function GalleryPage() {
{columnWorks.map((work) => {
const mediaPreviewUrl = work.thumbnailUrl || (work.url && !work.url.startsWith('data:') ? work.url : '');
return (
<div
key={work.id}
className="gallery-work-shell group"
style={getGalleryCardStyle(cardPalettes[work.id])}
>
<div className="gallery-glow-layer" aria-hidden="true" />
<Card
className={`${galleryGlassCard} gallery-work-card w-full overflow-hidden cursor-pointer !rounded-2xl !py-0`}
onClick={() => setSelectedWork(work)}
>
<div className="relative overflow-hidden bg-black/25">
{mediaPreviewUrl ? (
<img
src={mediaPreviewUrl}
alt={(work.prompt || '').slice(0, 30)}
className="block h-auto w-full object-contain"
loading="lazy"
onLoad={(e) => handleCardImageLoad(work.id, e)}
onDoubleClick={(e) => { e.stopPropagation(); setFullscreenSrc(work.url); }}
/>
) : (
<div className="flex aspect-square w-full flex-col items-center justify-center bg-gradient-to-br from-muted to-muted/50">
<Sparkles className="h-8 w-8 text-muted-foreground/20" />
</div>
)}
{isAdmin && apiWorkIds.has(work.id) && (
<button
className={`absolute left-2 top-2 z-20 flex h-7 w-7 items-center justify-center rounded-lg border text-xs font-semibold backdrop-blur-md transition-colors ${
selectedGalleryIds.has(work.id)
? 'border-primary/60 bg-primary text-primary-foreground'
: 'border-white/20 bg-black/45 text-white hover:bg-black/65'
}`}
onClick={(e) => toggleSelectGalleryWork(work.id, e)}
title={selectedGalleryIds.has(work.id) ? '取消选择' : '选择作品'}
<div
key={work.id}
className="gallery-work-shell group"
style={getGalleryCardStyle(cardPalettes[work.id])}
>
{selectedGalleryIds.has(work.id) ? '✓' : ''}
</button>
)}
{(work.type === 'video' || work.type === 'text2video' || work.type === 'img2video') && (
<Badge className={`absolute left-2 ${isAdmin && apiWorkIds.has(work.id) ? 'top-11' : 'top-2'}`} variant="secondary">
<Film className="h-3 w-3 mr-1" />
</Badge>
)}
<Badge className="absolute top-2 right-2" variant="secondary">
{getCategoryLabel(work)}
</Badge>
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-20 flex translate-y-2 justify-center gap-2 opacity-0 transition-all duration-300 ease-out group-hover:translate-y-0 group-hover:opacity-100">
<Button
size="sm"
variant="secondary"
className="gallery-work-action-button pointer-events-auto h-9 w-9 p-0"
onClick={(e) => toggleLike(work.id, e)}
title="点赞"
<Card
className={`${galleryGlassCard} gallery-work-card w-full overflow-hidden cursor-pointer !rounded-2xl !py-0`}
onClick={() => setSelectedWork(work)}
>
<Heart className={`h-4 w-4 ${likedIds.has(work.id) ? 'fill-current' : ''}`} />
</Button>
<Button
size="sm"
variant="secondary"
className="gallery-work-action-button pointer-events-auto h-9 w-9 p-0"
onClick={(e) => handleDownload(work.url, `miaojing-${work.id}.png`, e)}
title="下载"
>
<Download className="h-4 w-4" />
</Button>
{isAdmin && apiWorkIds.has(work.id) && (
<Button
size="sm"
variant="destructive"
className="pointer-events-auto h-9 w-9 rounded-full p-0 shadow-[0_12px_28px_rgba(0,0,0,0.34)]"
onClick={(e) => handleDeleteGalleryWorks([work.id], e)}
title="从画廊删除"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
<CardContent className="flex h-[152px] flex-col p-3">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center gap-2">
<div className="flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary/15 text-xs font-semibold text-primary ring-1 ring-primary/25">
{work.publisherAvatarUrl ? (
<img
src={work.publisherAvatarUrl}
alt={work.publisherNickname}
className="h-full w-full object-cover"
loading="lazy"
/>
) : (
getAvatarText(work.publisherNickname)
)}
</div>
<span className="truncate text-sm font-medium">
{work.publisherNickname}
</span>
<div className="relative overflow-hidden bg-black/25">
{mediaPreviewUrl ? (
<img
src={mediaPreviewUrl}
alt={(work.prompt || '').slice(0, 30)}
className="block h-auto w-full object-contain"
loading="lazy"
onLoad={(e) => handleCardImageLoad(work.id, e)}
onDoubleClick={(e) => { e.stopPropagation(); setFullscreenSrc(work.url); }}
/>
) : (
<div className="flex aspect-square w-full flex-col items-center justify-center bg-gradient-to-br from-muted to-muted/50">
<Sparkles className="h-8 w-8 text-muted-foreground/20" />
</div>
)}
{isAdmin && apiWorkIds.has(work.id) && (
<button
className={`absolute left-2 top-2 z-20 flex h-7 w-7 items-center justify-center rounded-lg border text-xs font-semibold backdrop-blur-md transition-colors ${
selectedGalleryIds.has(work.id)
? 'border-primary/60 bg-primary text-primary-foreground'
: 'border-white/20 bg-black/45 text-white hover:bg-black/65'
}`}
onClick={(e) => toggleSelectGalleryWork(work.id, e)}
title={selectedGalleryIds.has(work.id) ? '取消选择' : '选择作品'}
>
{selectedGalleryIds.has(work.id) ? '✓' : ''}
</button>
)}
{(work.type === 'video' || work.type === 'text2video' || work.type === 'img2video') && (
<Badge className={`absolute left-2 ${isAdmin && apiWorkIds.has(work.id) ? 'top-11' : 'top-2'}`} variant="secondary">
<Film className="h-3 w-3 mr-1" />
</Badge>
)}
<Badge className="absolute top-2 right-2" variant="secondary">
{getCategoryLabel(work)}
</Badge>
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-20 flex translate-y-2 justify-center gap-2 opacity-0 transition-all duration-300 ease-out group-hover:translate-y-0 group-hover:opacity-100">
<Button
size="sm"
variant="secondary"
className="gallery-work-action-button pointer-events-auto h-9 w-9 p-0"
onClick={(e) => toggleLike(work.id, e)}
title="点赞"
>
<Heart className={`h-4 w-4 ${likedIds.has(work.id) ? 'fill-current' : ''}`} />
</Button>
<Button
size="sm"
variant="secondary"
className="gallery-work-action-button pointer-events-auto h-9 w-9 p-0"
onClick={(e) => handleDownload(work.url, `miaojing-${work.id}.png`, e)}
title="下载"
>
<Download className="h-4 w-4" />
</Button>
{isAdmin && apiWorkIds.has(work.id) && (
<Button
size="sm"
variant="destructive"
className="pointer-events-auto h-9 w-9 rounded-full p-0 shadow-[0_12px_28px_rgba(0,0,0,0.34)]"
onClick={(e) => handleDeleteGalleryWorks([work.id], e)}
title="从画廊删除"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
<CardContent className="flex h-[152px] flex-col p-3">
<div className="flex items-center justify-between">
<div className="flex min-w-0 items-center gap-2">
<div className="flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary/15 text-xs font-semibold text-primary ring-1 ring-primary/25">
{work.publisherAvatarUrl ? (
<img
src={work.publisherAvatarUrl}
alt={work.publisherNickname}
className="h-full w-full object-cover"
loading="lazy"
/>
) : (
getAvatarText(work.publisherNickname)
)}
</div>
<span className="truncate text-sm font-medium">
{work.publisherNickname}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Heart className={`h-3 w-3 ${likedIds.has(work.id) ? 'fill-rose-500 text-rose-500' : ''}`} />
{work.likes + (likedIds.has(work.id) ? 1 : 0)}
</div>
</div>
<p className="mt-2 h-[100px] overflow-hidden whitespace-pre-wrap break-words text-xs leading-5 text-muted-foreground line-clamp-5">
{work.prompt}
</p>
</CardContent>
</Card>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Heart className={`h-3 w-3 ${likedIds.has(work.id) ? 'fill-rose-500 text-rose-500' : ''}`} />
{work.likes + (likedIds.has(work.id) ? 1 : 0)}
</div>
</div>
<p className="mt-2 h-[100px] overflow-hidden whitespace-pre-wrap break-words text-xs leading-5 text-muted-foreground line-clamp-5">
{work.prompt}
</p>
</CardContent>
</Card>
</div>
);
})}
</div>
@@ -1106,5 +1105,3 @@ export default function GalleryPage() {
</div>
);
}

View File

@@ -227,10 +227,10 @@
.gallery-work-shell::before {
content: "";
position: absolute;
inset: -2px;
inset: -3px;
z-index: -1;
padding: 2px;
border-radius: 1.12rem;
padding: 3px;
border-radius: 1.18rem;
background:
conic-gradient(
from 0deg,
@@ -260,10 +260,10 @@
.gallery-work-shell::after {
content: "";
position: absolute;
inset: -6px;
inset: -7px;
z-index: -2;
padding: 6px;
border-radius: 1.32rem;
padding: 7px;
border-radius: 1.38rem;
background:
conic-gradient(
from 0deg,
@@ -276,7 +276,7 @@
transparent 150deg,
transparent 360deg
);
filter: blur(6px) saturate(1.4);
filter: blur(7px) saturate(1.45);
opacity: 0;
transition: opacity 320ms ease;
animation: gallery-border-flow 3.6s linear infinite;
@@ -291,41 +291,6 @@
mask-composite: exclude;
}
/* 外围辉光层 */
.gallery-work-shell .gallery-glow-layer {
content: "";
position: absolute;
inset: -14px;
z-index: -3;
border-radius: 1.6rem;
background:
conic-gradient(
from 0deg,
transparent 0deg,
transparent 56deg,
var(--gallery-accent-1) 66deg,
var(--gallery-accent-2) 86deg,
var(--gallery-accent-3) 114deg,
var(--gallery-accent-1) 142deg,
transparent 150deg,
transparent 360deg
);
filter: blur(18px) saturate(1.8);
opacity: 0;
transition: opacity 420ms ease;
animation: gallery-border-flow 3.6s linear infinite;
animation-play-state: paused;
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
pointer-events: none;
}
.gallery-work-shell:hover {
z-index: 10;
transform: translateY(-6px) scale(1.02);
@@ -338,12 +303,7 @@
}
.gallery-work-shell:hover::after {
opacity: 0.55;
animation-play-state: running;
}
.gallery-work-shell:hover .gallery-glow-layer {
opacity: 0.32;
opacity: 0.64;
animation-play-state: running;
}