feat: add ExceptionListTable and ExceptionListView components

Phase 5 complete - extracted advanced list view from monolithic
ExceptionPage into focused table and container components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
liumangmang
2026-05-07 21:06:57 +08:00
parent ad9501fb11
commit 52c2e3dcc8
2 changed files with 188 additions and 0 deletions
@@ -0,0 +1,91 @@
// frontend/src/components/exceptions/ExceptionListTable.jsx
export default function ExceptionListTable({
items, selectedIds, onToggleSelect, onToggleAll,
isListLoading
}) {
if (isListLoading) {
return (
<div className="py-12 text-center text-slate-500">正在加载异常列表...</div>
);
}
if (!items.length) {
return (
<div className="py-12 text-center text-slate-500">当前筛选条件下没有异常记录</div>
);
}
return (
<table className="w-full border-separate border-spacing-y-2 text-left text-sm">
<thead className="sticky top-0 z-10 bg-slate-950/95 text-xs uppercase text-slate-500 backdrop-blur">
<tr>
<th className="w-12 px-4 py-3">
<input
type="checkbox"
className="rounded border-slate-700 bg-slate-950"
checked={selectedIds.length === items.length && items.length > 0}
onChange={(e) => onToggleAll(e.target.checked)}
disabled={!items.length}
/>
</th>
<th className="px-3 py-3 font-medium">异常文件</th>
<th className="px-3 py-3 font-medium">分类</th>
<th className="px-3 py-3 font-medium">状态</th>
<th className="px-3 py-3 font-medium">原因</th>
</tr>
</thead>
<tbody>
{items.map((item) => {
const checked = selectedIds.includes(item.exception_id);
const statusColor = {
open: 'border-amber-500/30 bg-amber-500/10 text-amber-200',
resolved: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200',
ignored: 'border-slate-500/30 bg-slate-500/10 text-slate-400'
}[item.exception_resolution_status] || '';
const statusLabel = {
open: '开放', resolved: '已解决', ignored: '已忽略'
}[item.exception_resolution_status] || item.exception_resolution_status;
const typeColor = {
missing_tags: 'border-amber-500/20 bg-amber-500/5 text-amber-300',
duplicates: 'border-slate-500/20 bg-slate-500/5 text-slate-300',
match_failed: 'border-rose-500/20 bg-rose-500/5 text-rose-300',
low_score: 'border-amber-500/20 bg-amber-500/5 text-amber-300',
convert_failed: 'border-rose-500/20 bg-rose-500/5 text-rose-300',
organize_failed: 'border-rose-500/20 bg-rose-500/5 text-rose-300'
}[item.exception_type] || '';
return (
<tr key={item.exception_id} className="group hover:bg-slate-800/30 transition-colors">
<td className="px-4 py-3">
<input
type="checkbox"
className="rounded border-slate-700 bg-slate-950"
checked={checked}
onChange={() => onToggleSelect(item.exception_id)}
/>
</td>
<td className="px-3 py-3">
<div className="font-medium text-slate-100">{item.display_title}</div>
<div className="text-xs text-slate-500">{item.filename}</div>
</td>
<td className="px-3 py-3">
<span className={`rounded-full border px-2 py-0.5 text-[11px] ${typeColor}`}>
{item.type_label}
</span>
</td>
<td className="px-3 py-3">
<span className={`rounded-full border px-2 py-0.5 text-[11px] ${statusColor}`}>
{statusLabel}
</span>
</td>
<td className="px-3 py-3 max-w-[200px] truncate text-xs text-slate-400">
{item.display_reason}
</td>
</tr>
);
})}
</tbody>
</table>
);
}
@@ -0,0 +1,97 @@
// frontend/src/components/exceptions/ExceptionListView.jsx
import { AlertTriangle, Music2 } from 'lucide-react';
import { BULK_ACTIONS, actionButtonClass } from '../../utils/exceptions';
import ExceptionStatsBar from './ExceptionStatsBar';
import ExceptionTypeNav from './ExceptionTypeNav';
import ExceptionListTable from './ExceptionListTable';
import Pagination from '../Pagination';
export default function ExceptionListView({
summary, summaryError,
items, total, isListLoading, listError,
selectedIds, onToggleSelect, onToggleAll,
activeFilter, onFilterChange,
resolutionFilter, onResolutionChange,
currentPage, onPageChange,
bulkState, onBulkAction,
onSwitchToWizard
}) {
const totalPages = Math.max(1, Math.ceil(total / 8));
return (
<div className="relative flex h-[calc(100vh-120px)] flex-col gap-6 py-6 xl:flex-row">
<section className="relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[28px] border border-slate-800/90 bg-[radial-gradient(circle_at_top_left,_rgba(99,102,241,0.08),_transparent_30%),linear-gradient(180deg,rgba(15,23,42,0.98),rgba(2,6,23,0.98))] shadow-[0_24px_80px_rgba(2,6,23,0.45)] xl:basis-[66%]">
<div className="border-b border-slate-800/80 px-6 py-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs uppercase tracking-[0.28em] text-indigo-300/70">异常决策台</p>
<h2 className="-mt-1 flex items-center gap-3 text-2xl font-semibold text-white">
<AlertTriangle className="h-6 w-6 text-rose-400" />
高级异常处理
</h2>
<p className="mt-2 max-w-2xl text-sm text-slate-400">
批量重复文件转码失败和入库失败在这里处理保留完整筛选与决策台能力
</p>
{summaryError ? <p className="mt-3 text-xs text-amber-300">{summaryError}</p> : null}
</div>
<div className="space-y-3">
<ExceptionStatsBar summary={summary} viewMode="advanced" />
<button onClick={onSwitchToWizard} className={actionButtonClass(true)}>
<Music2 className="h-3.5 w-3.5" />
返回单曲向导
</button>
</div>
</div>
<div className="mt-5">
<ExceptionTypeNav
activeFilter={activeFilter}
onFilterChange={(id) => { onFilterChange(id); onPageChange(1); }}
resolutionFilter={resolutionFilter}
onResolutionChange={(id) => { onResolutionChange(id); onPageChange(1); }}
summary={summary}
/>
</div>
{!bulkState.disabled && selectedIds.length > 0 && (
<div className="mt-4 flex items-center gap-3 rounded-2xl border border-indigo-900/40 bg-indigo-950/20 p-3">
<span className="text-xs text-indigo-200">已选 {selectedIds.length} </span>
{bulkState.actions.map((action) => (
<button
key={action}
onClick={() => onBulkAction(action)}
className="rounded-xl bg-indigo-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-400"
>
{{ retry_match: '一键匹配', ignore_exception: '全部忽略', retry_organize: '重试入库' }[action] || action}
</button>
))}
</div>
)}
</div>
<div className="min-h-0 flex-1 overflow-auto px-3 pb-4 pt-2">
<ExceptionListTable
items={items}
selectedIds={selectedIds}
onToggleSelect={onToggleSelect}
onToggleAll={onToggleAll}
isListLoading={isListLoading}
/>
{listError && (
<div className="rounded-2xl border border-rose-900/50 bg-rose-950/20 p-4 text-sm text-rose-200 mt-4">
{listError}
</div>
)}
</div>
<div className="border-t border-slate-800/80 px-6 py-4">
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
</div>
</section>
</div>
);
}