Files
beige/frontend/src/app/history/page.tsx
yangqianqian 1b488752e6 B6/B7: failed终态前端可见 + SSE事件链核实
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
2026-06-16 12:45:13 +08:00

96 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}