Merge pull request '修复首页失效封面与 Logo 的破图显示' (#8) from codex/home-image-recovery into main
This commit is contained in:
commit
c35f5fc5a8
52
components/home/lovart/home-image.tsx
Normal file
52
components/home/lovart/home-image.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
type HomeImageProps = {
|
||||
sources: Array<string | null | undefined>;
|
||||
alt: string;
|
||||
className?: string;
|
||||
imageClassName?: string;
|
||||
fallback?: ReactNode;
|
||||
loading?: "lazy" | "eager";
|
||||
onUnavailable?: () => void;
|
||||
};
|
||||
|
||||
/** Keep failed image elements out of view, trying each distinct source once. */
|
||||
export function HomeImage(props: HomeImageProps) {
|
||||
const sources = [...new Set(props.sources.filter((url): url is string => Boolean(url?.trim())))];
|
||||
return <HomeImageAttempt key={JSON.stringify(sources)} {...props} sources={sources} />;
|
||||
}
|
||||
|
||||
function HomeImageAttempt({
|
||||
sources, alt, className, imageClassName, fallback, loading = "lazy", onUnavailable,
|
||||
}: Omit<HomeImageProps, "sources"> & { sources: string[] }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const source = sources[index];
|
||||
return (
|
||||
<span className={`relative block overflow-hidden ${className || ""}`}>
|
||||
{!loaded && (
|
||||
<span className={fallback ? "block" : "block aspect-[4/5] rounded-[12px] bg-[#e8e8e8] dark:bg-[#1b1c20]"}>
|
||||
{fallback}
|
||||
</span>
|
||||
)}
|
||||
{source ? (
|
||||
<img
|
||||
key={source}
|
||||
src={source}
|
||||
alt={alt}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
className={`${imageClassName || ""} ${loaded ? "" : "absolute inset-0 opacity-0"}`}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => {
|
||||
setLoaded(false);
|
||||
setIndex(index + 1);
|
||||
if (index + 1 >= sources.length) onUnavailable?.();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { ReferenceIcon } from "@/components/lovart/reference-icon";
|
||||
import { Gem, Heart, Loader2, PlayCircle } from "lucide-react";
|
||||
import type { Ref } from "react";
|
||||
import { useState, type Ref } from "react";
|
||||
import { HomeImage } from "./home-image";
|
||||
import type { FeaturedImage } from "./types";
|
||||
|
||||
type LovartFeaturedGalleryProps = {
|
||||
@ -30,6 +31,10 @@ function getFeaturedPreviewUrl(item: FeaturedImage) {
|
||||
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,
|
||||
@ -42,6 +47,12 @@ export function LovartFeaturedGallery({
|
||||
isLoadingMoreFeatured,
|
||||
setActiveCategory,
|
||||
}: LovartFeaturedGalleryProps) {
|
||||
const [unavailable, setUnavailable] = useState<Set<string>>(() => new Set());
|
||||
const [retryVersion, setRetryVersion] = useState(0);
|
||||
const visibleItems = featuredItems.filter((item) =>
|
||||
Boolean(item.thumbnailUrl || item.originalUrl) && !unavailable.has(imageIdentity(item)),
|
||||
);
|
||||
const unavailableCount = featuredItems.length - visibleItems.length;
|
||||
return (
|
||||
<section
|
||||
ref={featuredSectionRef}
|
||||
@ -88,43 +99,44 @@ export function LovartFeaturedGallery({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{featuredItems.length === 0 ? (
|
||||
{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">
|
||||
{featuredItems.map((item, index) => (
|
||||
{visibleItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
key={`${imageIdentity(item)}:${retryVersion}`}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
window.open(item.originalUrl || item.thumbnailUrl, "_blank")
|
||||
}
|
||||
className="group mb-[24px] block w-full cursor-pointer break-inside-avoid text-left"
|
||||
>
|
||||
<img
|
||||
src={getFeaturedPreviewUrl(item)}
|
||||
<HomeImage
|
||||
sources={[getFeaturedPreviewUrl(item), item.thumbnailUrl, item.originalUrl]}
|
||||
alt={item.prompt || "灵感图"}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
const image = event.currentTarget;
|
||||
const fallbackUrl = item.originalUrl || item.thumbnailUrl;
|
||||
|
||||
if (
|
||||
!fallbackUrl ||
|
||||
image.dataset.fallbackApplied === "true"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.dataset.fallbackApplied = "true";
|
||||
image.src = fallbackUrl;
|
||||
}}
|
||||
className="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]"
|
||||
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">
|
||||
@ -138,11 +150,11 @@ export function LovartFeaturedGallery({
|
||||
<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 + index * 37).toLocaleString()}
|
||||
{(1200 + featuredItems.indexOf(item) * 37).toLocaleString()}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-[4px]">
|
||||
<Heart className="h-[11px] w-[11px]" strokeWidth={2} />
|
||||
{(20 + (index % 9) * 11).toLocaleString()}
|
||||
{(20 + (featuredItems.indexOf(item) % 9) * 11).toLocaleString()}
|
||||
</span>
|
||||
<Gem className="h-[11px] w-[11px]" strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { LovartHomeSidebarItemKey } from "@/lib/home/lovart-sidebar-config";
|
||||
import type { NavLink, TutorialVideo } from "./types";
|
||||
import { HomeImage } from "./home-image";
|
||||
|
||||
type SidebarItem = {
|
||||
iconUrl?: string | null;
|
||||
@ -183,17 +184,18 @@ export function LovartSidebar({
|
||||
onClick={onHome}
|
||||
title={siteName}
|
||||
>
|
||||
{siteLogo ? (
|
||||
<img
|
||||
src={siteLogo}
|
||||
<HomeImage
|
||||
sources={[siteLogo]}
|
||||
alt={siteName}
|
||||
loading="eager"
|
||||
className="h-7 w-7 rounded-full object-cover"
|
||||
imageClassName="h-7 w-7 rounded-full object-cover"
|
||||
fallback={
|
||||
<span aria-hidden="true" className="flex h-7 w-7 items-center justify-center rounded-full bg-black text-[12px] font-semibold text-white dark:bg-white dark:text-black">
|
||||
{siteName.slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-black text-[12px] font-semibold text-white dark:bg-white dark:text-black">
|
||||
{siteName.slice(0, 1).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{siteName}</TooltipContent>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user