'use client'; /** * HistoryTable / HistorySkeleton / Pagination / DownloadButton * 历史归档页子组件(按约束拆出 ≤100行) * * C1修复:下载走 fetch+blob,不用 window.open。 * window.open 无法附带 Authorization header,会 401。 * 改法:api.getBlob 拿二进制流 → URL.createObjectURL → a.click → revoke。 */ import { useState, Fragment } from 'react'; import Link from 'next/link'; import { clsx } from 'clsx'; import { TaskListItem } from '@/types'; import { api } from '@/lib/api'; const STATUS_LABEL: Record = { approved: { text: '已通过', color: 'text-green-600 bg-green-50' }, archived: { text: '已归档', color: 'text-gray-500 bg-gray-100' }, rejected: { text: '被打回', color: 'text-red-500 bg-red-50' }, failed: { text: '生成失败', color: 'text-red-600 bg-red-50' }, fission: { text: '裂变', color: 'text-orange-600 bg-orange-50' }, }; // --- DownloadButton --- 触发打包→轮询→下载 zip function DownloadButton({ taskId }: { taskId: number }) { const [state, setState] = useState<'idle' | 'packaging' | 'done' | 'error'>('idle'); const [pkgId, setPkgId] = useState(null); async function handleDownload() { setState('packaging'); try { // 1. 触发打包 const pkg = await api.post<{ delivery_package_id: number }>(`/api/v1/tasks/${taskId}/package`, {}); const id = pkg.delivery_package_id; setPkgId(id); // 2. 轮询 status(最多20次,每2s) for (let i = 0; i < 20; i++) { await new Promise((r) => setTimeout(r, 2000)); const info = await api.get<{ status: string }>(`/api/v1/delivery-packages/${id}/download`); if (info.status === 'ready' || info.status === 'downloaded') { // 3. fetch+blob 下载(带 Authorization header,避免 window.open 401) const blob = await api.getBlob(`/api/v1/delivery-packages/${id}/download-file`); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `delivery-${id}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); await api.post(`/api/v1/delivery-packages/${id}/mark-downloaded`); setState('done'); return; } } setState('error'); } catch { setState('error'); } } if (state === 'done') return ( ✓ 已下载到「下载」文件夹 ); if (state === 'error') return 失败,刷新重试; return ( ); } function FissionDownloadButton({ fissionId }: { fissionId: number }) { const [state, setState] = useState<'idle' | 'downloading' | 'done' | 'error'>('idle'); async function handleDownload() { setState('downloading'); try { const blob = await api.getBlob(`/api/v1/fission/${fissionId}/download`); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `fission-${fissionId}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setState('done'); } catch { setState('error'); } } if (state === 'done') return ✓ 已下载; if (state === 'error') return 失败,刷新重试; return ( ); } // --- HistoryTable --- export function HistoryTable({ tasks }: { tasks: TaskListItem[] }) { return (
{tasks.map((task, i) => ( {/* 被打回任务:原因独占一行展示,运营见"为何被打回"可重做(R8历史复用) */} {task.status === 'rejected' && task.reject_reason && ( )} ))}
任务 产品 状态 文案 图片 创建时间 操作
{task.theme} {task.product_name ?? '-'} {STATUS_LABEL[task.status]?.text ?? task.status} {task.text_count} 条 {task.image_count} 张 {new Date(task.created_at).toLocaleDateString('zh-CN')}
查看 {task.item_type !== 'fission' && task.status !== 'failed' && ( 排图 )} {task.status === 'approved' && } {task.item_type === 'fission' && }
打回原因:{task.reject_reason}
); } // --- HistorySkeleton --- export function HistorySkeleton() { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
); } // --- Pagination --- export function Pagination({ page, total, onChange, }: { page: number; total: number; onChange: (p: number) => void }) { return (
第 {page} / {total} 页
); }