B6 错误态: - 后端已加 TaskStatus.FAILED + 异常区分重试中/耗尽(014迁移ALTER status ENUM) - 前端 TaskStatus 类型加 failed - RecentTasksBar/history STATUS_LABEL 补 failed 标签 - history 页 HISTORY_STATUSES 纳入 failed(否则失败任务前端找不到) - FatalErrorBanner + useSse error 不再静默丢弃 B7: 核实 SSE(ticket换JWT) + 事件消费链无缺口,标绿 tsc EXIT=0
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
'use client';
|
||
/**
|
||
* 历史归档 /history
|
||
* 角色:熟手 + 组长(管理员也能进)
|
||
* 数据:GET /api/v1/tasks?status=approved,archived,rejected
|
||
*/
|
||
import { useEffect, useState, useCallback } from 'react';
|
||
import { api } from '@/lib/api';
|
||
import { TaskListItem, PaginatedResponse } from '@/types';
|
||
import { getErrorAction } from '@/types/errors';
|
||
import { clsx } from 'clsx';
|
||
import { HistoryTable, HistorySkeleton, Pagination } from './components';
|
||
|
||
const HISTORY_STATUSES = ['approved', 'archived', 'rejected', 'failed'];
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
approved: '已通过', archived: '已归档', rejected: '被打回', failed: '生成失败',
|
||
};
|
||
const PAGE_SIZE = 20;
|
||
|
||
export default function HistoryPage() {
|
||
const [tasks, setTasks] = useState<TaskListItem[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [page, setPage] = useState(1);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||
|
||
const fetchHistory = useCallback(async (p: number, status: string) => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const params = new URLSearchParams({ page: String(p), page_size: String(PAGE_SIZE) });
|
||
if (status !== 'all') params.append('status', status);
|
||
else HISTORY_STATUSES.forEach((s) => params.append('status', s));
|
||
const res = await api.get<PaginatedResponse<TaskListItem>>(`/api/v1/tasks?${params}`);
|
||
setTasks(res.items);
|
||
setTotal(res.total);
|
||
} catch (e: unknown) {
|
||
const ae = e as { code?: number };
|
||
setError(getErrorAction(ae.code ?? 50001).message);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => { fetchHistory(page, statusFilter); }, [page, statusFilter, fetchHistory]);
|
||
|
||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-semibold text-text-primary">历史归档</h1>
|
||
<span className="text-sm text-text-tertiary">共 {total} 条</span>
|
||
</div>
|
||
|
||
{/* 状态筛选 */}
|
||
<div className="flex gap-2" role="group" aria-label="状态筛选">
|
||
{[{ key: 'all', label: '全部' }, ...HISTORY_STATUSES.map((s) => ({
|
||
key: s, label: STATUS_LABELS[s],
|
||
}))].map((opt) => (
|
||
<button
|
||
key={opt.key}
|
||
onClick={() => { setStatusFilter(opt.key); setPage(1); }}
|
||
className={clsx(
|
||
'px-3 py-1 rounded-full text-sm border transition-colors',
|
||
statusFilter === opt.key
|
||
? 'bg-brand-orange text-white border-brand-orange'
|
||
: 'border-border-default text-text-secondary hover:border-brand-orange'
|
||
)}
|
||
>
|
||
{opt.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
|
||
{error}
|
||
<button className="ml-4 underline" onClick={() => fetchHistory(page, statusFilter)}>
|
||
重试
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{loading ? <HistorySkeleton /> : tasks.length === 0 ? (
|
||
<div className="py-20 text-center text-text-tertiary text-sm">暂无历史记录</div>
|
||
) : (
|
||
<HistoryTable tasks={tasks} />
|
||
)}
|
||
|
||
{totalPages > 1 && <Pagination page={page} total={totalPages} onChange={setPage} />}
|
||
</div>
|
||
);
|
||
}
|