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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user