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 {alt}; } 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 (

产品档案后台

产品档案包含名单位置数据,请输入生产订单管理口令。

setPassword(event.target.value)} /> {loginError &&
{loginError}
}
); } 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 && (
{previewUrl ? :
暂无预览图
}
归档词云 {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}
)} )}
); }