feat: focus operations review queue

This commit is contained in:
lingniu
2026-07-19 08:48:32 +08:00
parent c3881bf25c
commit 26ccd2434a
6 changed files with 354 additions and 28 deletions

View File

@@ -82,9 +82,14 @@ test('renders reconciliation queue, loads evidence on demand and records review
expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument();
expect(screen.getByText('质量问题队列').closest('.semi-card')).toHaveClass('v2-reconcile-center');
expect(screen.getByText('质量问题队列').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading');
expect(document.querySelector('.v2-reconcile-kpi-card small')?.textContent).toBe('待复核');
expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(4);
expect(screen.getByText('SLA 超时').closest('.semi-card')).toHaveClass('v2-reconcile-kpi-card', 'is-sla', 'is-overdue');
const reconciliationMetrics = screen.getByRole('list', { name: '差异处置队列概览' });
expect(reconciliationMetrics.closest('.semi-card')).toHaveClass('v2-workspace-metric-rail', 'v2-reconcile-metric-rail');
expect(within(reconciliationMetrics).getAllByRole('listitem')).toHaveLength(4);
expect(within(reconciliationMetrics).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary');
expect(within(reconciliationMetrics).getByText('SLA 超时').closest('[role="listitem"]')).toHaveClass('is-danger');
expect(within(reconciliationMetrics).getByText('已自动恢复').closest('[role="listitem"]')).toHaveClass('is-secondary', 'is-success');
expect(screen.getByText('当前筛选').closest('.v2-workspace-metric-context')).toHaveTextContent('1');
expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(0);
expect(screen.queryByText('活跃问题', { exact: true })).not.toBeInTheDocument();
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
const trendButton = screen.getByRole('button', { name: '30 天趋势' });
@@ -169,6 +174,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar
const toolbar = document.querySelector('.v2-reconcile-toolbar');
expect(toolbar).toHaveClass('is-mobile-collapsed');
expect(screen.getByRole('button', { name: /修改筛选差异:活跃差异 · 1 条/ })).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /修改筛选差异/ }));
expect(toolbar).not.toHaveClass('is-mobile-collapsed');

View File

@@ -8,6 +8,7 @@ import { InlineError } from '../shared/AsyncState';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { PlatformTime } from '../shared/PlatformTime';
import { TablePagination } from '../shared/TablePagination';
import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
import { detailTriggerRow } from '../shared/detailTriggerRow';
@@ -312,7 +313,7 @@ export default function ReconciliationCenter() {
const totalPages = Math.max(1, Math.ceil((page?.total ?? 0) / pageSize));
const issueRows = page?.items ?? [];
const activeFilterCount = Number(Boolean(deferredKeyword)) + Number(severity !== 'all') + Number(ruleCode !== 'all');
const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)}${activeFilterCount ? ` · 另 ${activeFilterCount}` : ''}`;
const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)} · ${(page?.total ?? 0).toLocaleString('zh-CN')} ${activeFilterCount ? ` · 另 ${activeFilterCount}` : ''}`;
const selectedIssue = detail.data ?? issueRows.find((item) => item.id === selectedID);
const detailPanel = selectedID && detail.isPending
? <Card className="v2-reconcile-detail is-sheet" bodyStyle={{ padding: 0 }}><div className="v2-reconcile-detail-loading" role="status"><Spin size="middle" tip="正在读取差异证据…" /></div></Card>
@@ -350,14 +351,39 @@ export default function ReconciliationCenter() {
</>}
/>
{summary.isError ? <InlineError message={summary.error.message} onRetry={() => summary.refetch()} /> : null}
<div className="v2-reconcile-overview">
<div className="v2-reconcile-kpis">
<Card className="v2-reconcile-kpi-card is-priority" bodyStyle={{ padding: 0 }}><small>待复核</small><strong>{data?.pending.toLocaleString('zh-CN') ?? '—'}</strong><span>活跃问题 {data?.active.toLocaleString('zh-CN') ?? '—'}</span></Card>
<Card className={`v2-reconcile-kpi-card is-sla${data?.overSla ? ' is-overdue' : ''}`} bodyStyle={{ padding: 0 }}><small>SLA 超时</small><strong>{data?.overSla.toLocaleString('zh-CN') ?? '—'}</strong><span>{data?.overSla ? '超过 24 小时,优先复核' : '当前没有超时差异'}</span></Card>
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>已确认来源</small><strong>{data?.confirmed.toLocaleString('zh-CN') ?? '—'}</strong><span>保留为活跃差异</span></Card>
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>已自动恢复</small><strong>{data?.recovered.toLocaleString('zh-CN') ?? '—'}</strong><span>规则已不再命中</span></Card>
</div>
</div>
<WorkspaceMetricRail
variant="queue"
className="v2-reconcile-metric-rail"
ariaLabel="差异处置队列概览"
items={[
{
label: '待复核',
value: data?.pending.toLocaleString('zh-CN') ?? '—',
note: ` ${data?.active.toLocaleString('zh-CN') ?? '—'} `,
tone: 'primary'
},
{
label: 'SLA 超时',
value: data?.overSla.toLocaleString('zh-CN') ?? '—',
note: data?.overSla ? '超过 24 小时' : '当前无超时',
tone: data?.overSla ? 'danger' : 'success'
},
{
label: '已确认来源',
value: data?.confirmed.toLocaleString('zh-CN') ?? '—',
note: '保留活跃差异',
emphasis: 'secondary'
},
{
label: '已自动恢复',
value: data?.recovered.toLocaleString('zh-CN') ?? '—',
note: '规则不再命中',
tone: 'success',
emphasis: 'secondary'
}
]}
context={<span aria-live="polite"><small>当前筛选</small><strong>{(page?.total ?? 0).toLocaleString('zh-CN')}</strong><em>按严重程度、最近发现时间排序</em></span>}
/>
<div className="v2-reconcile-layout">
<div className="v2-reconcile-main">
<MobileFilterToggle
@@ -387,10 +413,6 @@ export default function ReconciliationCenter() {
...(data?.byRule.map((item) => ({ value: item.name, label: `${ruleLabel(item.name)} · ${item.count}` })) ?? [])
]} />
</div>
<div className="v2-reconcile-result-bar" aria-live="polite">
<span><strong>{(page?.total ?? 0).toLocaleString('zh-CN')}</strong> 个符合条件的问题</span>
<span>按严重程度、最近发现时间排序</span>
</div>
{issues.isError ? <InlineError message={issues.error.message} onRetry={() => issues.refetch()} /> : null}
<div
className="v2-reconcile-table-wrap is-scroll-region"