fix: load authenticated product archive images

This commit is contained in:
2026-09-12 18:37:56 +08:00
parent 4fd6d66c0e
commit 7e275798e7
2 changed files with 68 additions and 3 deletions
+58 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import AppSettingsWindow, { type ThemeMode } from '../components/AppSettingsWindow'; import AppSettingsWindow, { type ThemeMode } from '../components/AppSettingsWindow';
import { ensureOk } from '../lib/api'; import { apiUrl, ensureOk } from '../lib/api';
import { IconArchive, IconGrid, IconHelp, IconLock, IconRefresh, IconTrash } from '../components/Icons'; import { IconArchive, IconGrid, IconHelp, IconLock, IconRefresh, IconTrash } from '../components/Icons';
import { import {
deleteProduct, deleteProduct,
@@ -57,6 +57,51 @@ function formatDateTime(value: string): string {
}); });
} }
function AuthenticatedImage({
token,
imageUrl,
alt,
emptyLabel,
}: {
token: string;
imageUrl: string;
alt: string;
emptyLabel: string;
}) {
const [objectUrl, setObjectUrl] = useState('');
useEffect(() => {
let cancelled = false;
let createdUrl = '';
const load = async () => {
try {
const res = await fetch(apiUrl(imageUrl), {
headers: { Authorization: `Bearer ${token}` },
});
await ensureOk(res, '预览图读取失败');
const blob = await res.blob();
createdUrl = URL.createObjectURL(blob);
if (!cancelled) setObjectUrl(createdUrl);
} catch {
if (!cancelled) setObjectUrl('');
}
};
void load();
return () => {
cancelled = true;
if (createdUrl) URL.revokeObjectURL(createdUrl);
};
}, [imageUrl, token]);
if (!objectUrl) {
return <div className="product-archive-image-empty">{emptyLabel}</div>;
}
return <img src={objectUrl} alt={alt} loading="lazy" decoding="async" />;
}
export default function ProductArchivePage({ export default function ProductArchivePage({
themeMode, themeMode,
systemTheme, systemTheme,
@@ -286,7 +331,12 @@ export default function ProductArchivePage({
> >
<span className="product-archive-thumb"> <span className="product-archive-thumb">
{coverUrl {coverUrl
? <img src={coverUrl} alt={product.name} /> ? <AuthenticatedImage
token={token}
imageUrl={coverUrl}
alt={product.name}
emptyLabel="暂无预览图"
/>
: <span className="product-archive-thumb-empty"></span>} : <span className="product-archive-thumb-empty"></span>}
</span> </span>
<span className="product-archive-row-body"> <span className="product-archive-row-body">
@@ -352,7 +402,12 @@ export default function ProductArchivePage({
<div className="product-archive-detail"> <div className="product-archive-detail">
<div className="product-archive-cover"> <div className="product-archive-cover">
{previewUrl {previewUrl
? <img src={previewUrl} alt={`${selected.name} 预览图`} /> ? <AuthenticatedImage
token={token}
imageUrl={previewUrl}
alt={`${selected.name} 预览图`}
emptyLabel="暂无预览图"
/>
: <div className="product-archive-cover-empty"></div>} : <div className="product-archive-cover-empty"></div>}
</div> </div>
<div className="product-archive-detail-body"> <div className="product-archive-detail-body">
+10
View File
@@ -3687,6 +3687,16 @@ body.resizing .studio-panel {
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
.product-archive-image-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 11px;
text-align: center;
}
.product-archive-thumb-empty, .product-archive-thumb-empty,
.product-archive-cover-empty { .product-archive-cover-empty {
padding: 4px; padding: 4px;