s3.1080p.hk 已永久停用且文件不可恢复。首页查询排除旧源并统一分页统计,历史图册、聊天和画布保留记录并显示失效说明;下载和代理返回 410,停止旧源上传与归档。使用本地黑白 Logo 适配主题。 提供保存原配置后停用旧存储、取消失效精选、替换 Logo 的整理脚本。七牛 10 月 14 日上传核查记录见 docs/retired-storage.md。 验证:7 项旧存储检查、41 项下载和归档回归测试、TypeScript 检查通过。
184 lines
7.7 KiB
TypeScript
184 lines
7.7 KiB
TypeScript
import { ReferenceIcon } from "@/components/lovart/reference-icon";
|
||
import { Gem, Heart, Loader2, PlayCircle } from "lucide-react";
|
||
import { useState, type Ref } from "react";
|
||
import { HomeImage } from "./home-image";
|
||
import type { FeaturedImage } from "./types";
|
||
|
||
type LovartFeaturedGalleryProps = {
|
||
onOpenProjects: () => void;
|
||
onOpenMaterials: () => void;
|
||
activeCategory: string;
|
||
categories: string[];
|
||
featuredItems: FeaturedImage[];
|
||
featuredLoadMoreRef: Ref<HTMLDivElement>;
|
||
featuredSectionRef: Ref<HTMLElement>;
|
||
isLoadingFeatured: boolean;
|
||
isLoadingMoreFeatured: boolean;
|
||
setActiveCategory: (category: string) => void;
|
||
};
|
||
|
||
function getFeaturedPreviewUrl(item: FeaturedImage) {
|
||
const source = item.thumbnailUrl || item.originalUrl;
|
||
|
||
if (
|
||
!source ||
|
||
!/^https:\/\/yybb\.top\//i.test(source) ||
|
||
source.includes("?")
|
||
) {
|
||
return source;
|
||
}
|
||
|
||
return `${source}?imageView2/2/w/900/q/78`;
|
||
}
|
||
|
||
function imageIdentity(item: FeaturedImage) {
|
||
return JSON.stringify([item.id, item.thumbnailUrl, item.originalUrl]);
|
||
}
|
||
|
||
export function LovartFeaturedGallery({
|
||
onOpenProjects,
|
||
onOpenMaterials,
|
||
activeCategory,
|
||
categories,
|
||
featuredItems,
|
||
featuredLoadMoreRef,
|
||
featuredSectionRef,
|
||
isLoadingFeatured,
|
||
isLoadingMoreFeatured,
|
||
setActiveCategory,
|
||
}: LovartFeaturedGalleryProps) {
|
||
const [unavailable, setUnavailable] = useState<Set<string>>(() => new Set());
|
||
const [retryVersion, setRetryVersion] = useState(0);
|
||
const uniqueItems = [...new Map(featuredItems.map((item) => [item.id, item])).values()];
|
||
const visibleItems = uniqueItems.filter((item) =>
|
||
Boolean(item.thumbnailUrl || item.originalUrl),
|
||
);
|
||
const unavailableCount = uniqueItems.filter(item => unavailable.has(imageIdentity(item))).length;
|
||
return (
|
||
<section
|
||
ref={featuredSectionRef}
|
||
className="lovart-discovery mt-16 scroll-mt-28"
|
||
aria-label="灵感发现"
|
||
>
|
||
<div className="flex items-center gap-2 overflow-x-auto pb-2 text-[13px] text-[#858585] dark:text-white/50">
|
||
<span className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] bg-[#ededed] px-3 text-[#252525] dark:bg-white/10 dark:text-white">
|
||
<ReferenceIcon name="skill" size={16} />
|
||
灵感发现
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={onOpenProjects}
|
||
className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] px-3 hover:bg-black/5 dark:hover:bg-white/10"
|
||
>
|
||
<ReferenceIcon name="folder" size={16} />
|
||
我的项目
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onOpenMaterials}
|
||
className="inline-flex h-7 shrink-0 items-center gap-2 rounded-[8px] px-3 hover:bg-black/5 dark:hover:bg-white/10"
|
||
>
|
||
<ReferenceIcon name="brand" size={16} />
|
||
我的素材
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-x-5 overflow-x-auto border-b border-[#ededed] px-3 dark:border-white/10">
|
||
{categories.map((category) => (
|
||
<button
|
||
key={category}
|
||
type="button"
|
||
onClick={() => setActiveCategory(category)}
|
||
className={`relative h-9 shrink-0 border-b px-0 text-[13px] font-normal leading-none transition-colors duration-150 ${
|
||
activeCategory === category
|
||
? "border-[#303030] text-[#202020] dark:border-white dark:text-white"
|
||
: "border-transparent text-[#858585] hover:text-[#202020] dark:text-white/55 dark:hover:text-white"
|
||
}`}
|
||
>
|
||
{category}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{unavailableCount > 0 && (
|
||
<div role="status" className="mt-4 flex items-center gap-3 text-xs text-black/50 dark:text-white/50">
|
||
<span>{unavailableCount} 张灵感图片加载失败,请重试。</span>
|
||
<button
|
||
type="button"
|
||
className="shrink-0 underline underline-offset-4 hover:text-black dark:hover:text-white"
|
||
onClick={() => {
|
||
setUnavailable(new Set());
|
||
setRetryVersion((version) => version + 1);
|
||
}}
|
||
>
|
||
重试加载
|
||
</button>
|
||
</div>
|
||
)}
|
||
{visibleItems.length === 0 ? (
|
||
<div className="mt-[22px] rounded-[10px] border border-dashed border-black/15 bg-[#f7f7f7] p-10 text-center text-[14px] text-black/35 dark:border-white/12 dark:bg-[#141416] dark:text-white/35">
|
||
{isLoadingFeatured
|
||
? "灵感内容加载中..."
|
||
: unavailableCount > 0 ? "图片暂时无法加载,请稍后重试。" : "当前没有可展示的灵感数据。"}
|
||
</div>
|
||
) : (
|
||
<div className="mt-6 columns-1 gap-4 sm:columns-2 lg:columns-4">
|
||
{visibleItems.map((item) => (
|
||
<button
|
||
key={`${imageIdentity(item)}:${retryVersion}`}
|
||
type="button"
|
||
disabled={unavailable.has(imageIdentity(item))}
|
||
onClick={() =>
|
||
window.open(item.originalUrl || item.thumbnailUrl, "_blank")
|
||
}
|
||
className="group mb-[24px] block w-full cursor-pointer break-inside-avoid text-left"
|
||
>
|
||
{unavailable.has(imageIdentity(item)) ? (
|
||
<div className="flex aspect-[4/5] items-center justify-center rounded-[12px] bg-muted p-4 text-sm text-muted-foreground">图片加载失败</div>
|
||
) : <HomeImage
|
||
sources={[getFeaturedPreviewUrl(item), item.thumbnailUrl, item.originalUrl]}
|
||
alt={item.prompt || "灵感图"}
|
||
loading="lazy"
|
||
onUnavailable={() => setUnavailable((previous) => new Set(previous).add(imageIdentity(item)))}
|
||
imageClassName="block h-auto w-full rounded-[12px] bg-[#e8e8e8] object-cover transition-transform duration-150 ease-out will-change-transform group-hover:scale-[1.008] dark:bg-[#1b1c20]"
|
||
/>}
|
||
<div className="mt-[8px] flex h-[22px] items-center justify-between text-[11px] font-normal leading-none text-[#b8b8b8] dark:text-white/45">
|
||
<div className="flex min-w-0 items-center gap-[6px] pr-2">
|
||
<span className="flex h-[16px] w-[16px] shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#d8d8d8] text-[8px] font-medium text-white">
|
||
{(item.user?.name || "C").slice(0, 1).toUpperCase()}
|
||
</span>
|
||
<span className="line-clamp-1 text-[13px] font-normal leading-none text-[#4b4b4b] dark:text-white/80">
|
||
{item.user?.name || "创作者"}
|
||
</span>
|
||
</div>
|
||
<div className="inline-flex shrink-0 items-center gap-[7px] text-[11px] leading-none">
|
||
<span className="inline-flex items-center gap-[4px]">
|
||
<PlayCircle className="h-[11px] w-[11px]" strokeWidth={2} />
|
||
{(1200 + featuredItems.indexOf(item) * 37).toLocaleString()}
|
||
</span>
|
||
<span className="inline-flex items-center gap-[4px]">
|
||
<Heart className="h-[11px] w-[11px]" strokeWidth={2} />
|
||
{(20 + (featuredItems.indexOf(item) % 9) * 11).toLocaleString()}
|
||
</span>
|
||
<Gem className="h-[11px] w-[11px]" strokeWidth={2} />
|
||
</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
<div
|
||
ref={featuredLoadMoreRef}
|
||
className="mt-3 flex h-10 items-center justify-center"
|
||
>
|
||
{isLoadingMoreFeatured ? (
|
||
<div className="inline-flex items-center gap-2 text-sm text-black/45 dark:text-white/45">
|
||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||
正在加载更多灵感...
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|