fix: render prompt museum media

This commit is contained in:
2026-08-18 12:32:34 +08:00
parent 11b19832a3
commit f8d82e6c19
9 changed files with 609 additions and 37 deletions

View File

@@ -26,6 +26,7 @@ import {
} from '@/components/ui/sheet';
import {
fetchPromptMuseumEntry,
fetchPromptMuseumMedia,
fetchPromptMuseumPage,
PromptMuseumApiError,
} from '@/lib/image-prompt-museum';
@@ -84,6 +85,17 @@ function errorMessage(error: unknown): string {
return '获取灵感暂时不可用,请稍后再试';
}
function directHttpsUrl(value: string): string | null {
try {
const parsed = new URL(value);
return parsed.protocol === 'https:' && !parsed.username && !parsed.password
? parsed.toString()
: null;
} catch {
return null;
}
}
function MuseumImage({
image,
className,
@@ -93,22 +105,52 @@ function MuseumImage({
className?: string;
sizes?: string;
}) {
const [failed, setFailed] = useState(false);
if (failed || !image.url) {
const directSource = directHttpsUrl(image.url);
const [failedUrl, setFailedUrl] = useState<string | null>(null);
const [mediaState, setMediaState] = useState<{
imageUrl: string;
source: string | null;
failed: boolean;
}>({ imageUrl: '', source: null, failed: false });
useEffect(() => {
if (directHttpsUrl(image.url)) return;
let cancelled = false;
void fetchPromptMuseumMedia(image.url).then(
(dataUrl) => {
if (!cancelled) setMediaState({ imageUrl: image.url, source: dataUrl, failed: false });
},
() => {
if (!cancelled) setMediaState({ imageUrl: image.url, source: null, failed: true });
},
);
return () => {
cancelled = true;
};
}, [image.url]);
const relativeState = mediaState.imageUrl === image.url ? mediaState : null;
const source = directSource ?? relativeState?.source ?? null;
const failed = failedUrl === image.url || relativeState?.failed === true;
if (failed || !source) {
return (
<div className={cn('flex items-center justify-center bg-surface-subtle text-muted-foreground', className)}>
<div
aria-label={failed ? `${image.alt}加载失败` : `${image.alt}正在加载`}
className={cn('flex items-center justify-center bg-surface-subtle text-muted-foreground', className)}
>
<ImageIcon className="h-8 w-8" aria-hidden="true" />
</div>
);
}
return (
<img
src={image.url}
src={source}
alt={image.alt}
sizes={sizes}
loading="lazy"
className={className}
onError={() => setFailed(true)}
onError={() => setFailedUrl(image.url)}
/>
);
}
@@ -227,15 +269,17 @@ function AttributionBlock({ entry }: { entry: PromptMuseumEntry }) {
<div className="flex gap-3">
<dt className="w-12 shrink-0 text-muted-foreground"></dt>
<dd className="min-w-0 truncate font-medium text-foreground">
<a
href={entry.attribution.source.url}
target="_blank"
rel="noreferrer"
className="inline-flex max-w-full items-center gap-1 text-brand hover:underline"
>
<span className="truncate">{entry.attribution.source.name}</span>
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
</a>
{entry.attribution.source.url ? (
<a
href={entry.attribution.source.url}
target="_blank"
rel="noreferrer"
className="inline-flex max-w-full items-center gap-1 text-brand hover:underline"
>
<span className="truncate">{entry.attribution.source.name}</span>
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
</a>
) : entry.attribution.source.name}
</dd>
</div>
<div className="flex gap-3">