import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import AppSettingsWindow, { type ThemeMode } from '../components/AppSettingsWindow';
import { apiUrl, ensureOk } from '../lib/api';
import { IconArchive, IconGrid, IconHelp, IconLock, IconRefresh, IconTrash } from '../components/Icons';
import { wordCloudBoardUrl, type WordCloudBoardRoute } from '../lib/wordcloudBoard';
import {
deleteProduct,
getProduct,
listProducts,
restoreProduct,
} from '../lib/productArchive';
import type { ProductDetail, ProductStatus, ProductSummary } from '../types';
interface ProductArchivePageProps {
themeMode: ThemeMode;
systemTheme: 'light' | 'dark';
onThemeModeChange: (mode: ThemeMode) => void;
onOpenHome: () => void;
onOpenHelp?: () => void;
}
const TOKEN_KEY = 'wordcloud-orders-token';
const wordCloudBoardActions: Array<{ label: string; route: WordCloudBoardRoute }> = [
{ label: '个人查找', route: 'cloud' },
{ label: '现场大屏', route: 'screen' },
{ label: '手机控制', route: 'control' },
];
function statusClass(status: ProductStatus): string {
if (status === 'active') return 'product-status-archived';
if (status === 'pending_cleanup' || status === 'failed_cleanup') return 'product-status-pending-cleanup';
return 'product-status-empty';
}
function statusLabel(product: ProductSummary): string {
if (product.status === 'active') return '已归档词云数据';
if (product.status === 'pending_cleanup') return '待清理';
if (product.status === 'failed_cleanup') return '清理失败';
return '已清理';
}
function formatDate(value: string | null): string {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
}
function formatDateTime(value: string): string {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function hasWordCloudArchive(detail: ProductDetail): boolean {
return detail.versions.some(version =>
version.wordcloud_archives.some(archive => archive.source_job_id.trim().length > 0),
);
}
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
{emptyLabel}
;
}
return
;
}
export default function ProductArchivePage({
themeMode,
systemTheme,
onThemeModeChange,
onOpenHome,
onOpenHelp,
}: ProductArchivePageProps) {
const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY) || '');
const [password, setPassword] = useState('');
const [loginError, setLoginError] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [products, setProducts] = useState([]);
const [query, setQuery] = useState('');
const [selectedId, setSelectedId] = useState(null);
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [actionBusy, setActionBusy] = useState(false);
const selected = products.find(product => product.product_id === selectedId) || null;
const loadProducts = async (authToken: string, search = query) => {
if (!authToken) return;
setLoading(true);
setError('');
try {
const items = await listProducts(authToken, search);
setProducts(items);
setSelectedId(current => items.some(item => item.product_id === current) ? current : null);
} catch (err) {
setError(err instanceof Error ? err.message : '产品档案读取失败');
if (err instanceof Error && (err.message.includes('401') || err.message.includes('403'))) {
localStorage.removeItem(TOKEN_KEY);
setToken('');
}
} finally {
setLoading(false);
}
};
useEffect(() => {
void loadProducts(token);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
let cancelled = false;
if (!token || !selectedId) {
setDetail(null);
return;
}
setDetailLoading(true);
setError('');
getProduct(token, selectedId)
.then(next => {
if (!cancelled) setDetail(next);
})
.catch(err => {
if (cancelled) return;
setError(err instanceof Error ? err.message : '产品详情读取失败');
setDetail(null);
})
.finally(() => {
if (!cancelled) setDetailLoading(false);
});
return () => {
cancelled = true;
};
}, [selectedId, token]);
const handleLogin = async (event: FormEvent) => {
event.preventDefault();
setLoginError('');
try {
const res = await ensureOk(await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
}), '登录失败');
const data = await res.json() as { token: string };
localStorage.setItem(TOKEN_KEY, data.token);
setPassword('');
setToken(data.token);
} catch (err) {
setLoginError(err instanceof Error ? err.message : '登录失败');
}
};
const runAction = async (action: () => Promise) => {
if (!token || !selectedId || actionBusy) return;
setActionBusy(true);
setError('');
try {
await action();
await loadProducts(token);
const nextDetail = await getProduct(token, selectedId);
setDetail(nextDetail);
} catch (err) {
setError(err instanceof Error ? err.message : '操作失败');
} finally {
setActionBusy(false);
}
};
const copyProductId = async () => {
if (!detail) return;
try {
await navigator.clipboard.writeText(detail.product_id);
} catch {
setError('复制产品 ID 失败');
}
};
if (!token) {
return (
);
}
const detailImages = detail?.images || [];
const previewImage = detail
? detailImages.find(image => image.image_id === detail.cover_image_id) || detailImages[0]
: null;
const previewUrl = previewImage?.image_url || '';
const latestVersion = detail?.versions[detail.versions.length - 1] || null;
const canOpenWordCloudBoard = detail !== null
&& selected?.status === 'active'
&& hasWordCloudArchive(detail);
return (
{!selected ? (
在左侧选择产品查看详情
) : (
<>
{selected.name}
{statusLabel(selected)}
{[selected.sku, selected.specification].filter(Boolean).join(' · ') || '未填写规格'}
{(selected.status === 'pending_cleanup' || selected.status === 'failed_cleanup') && (
)}
{selected.status === 'active' && (
)}
{selected.status === 'pending_cleanup' && (
待清理 · 将于 {formatDate(selected.purge_after)} 删除。恢复产品可结束本次清理窗口。
)}
{selected.status === 'failed_cleanup' && (
清理失败,请人工检查档案目录后再恢复。
)}
{error && {error}
}
{detailLoading && 正在读取产品详情…
}
{detail && (
归档词云
{latestVersion?.wordcloud_count ?? 0} 份
设计版本
{detail.versions.length} 版
图片
{detail.images.length} 张
词云互动
{wordCloudBoardActions.map(action => (
canOpenWordCloudBoard ? (
{action.label}
) : (
)
))}
{!canOpenWordCloudBoard && (
暂无可用词云归档
)}
版本时间线
{detail.versions.length === 0 &&
暂无设计版本。
}
{detail.versions.map(version => (
{formatDateTime(version.created_at)}
{version.wordcloud_count} 份词云位置数据
))}
系统信息
产品 ID
{detail.product_id}
)}
>
)}
);
}