feat: unify governance evidence workbenches
This commit is contained in:
@@ -273,8 +273,10 @@ test('renders mobile access vehicles as selectable Semi cards', async () => {
|
|||||||
const governanceButton = screen.getByRole('button', { name: /接入治理/ });
|
const governanceButton = screen.getByRole('button', { name: /接入治理/ });
|
||||||
fireEvent.click(governanceButton);
|
fireEvent.click(governanceButton);
|
||||||
const governanceDialog = await screen.findByRole('dialog', { name: '接入治理配置' });
|
const governanceDialog = await screen.findByRole('dialog', { name: '接入治理配置' });
|
||||||
expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-config-sidesheet');
|
||||||
expect(document.querySelector('.v2-access-governance-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(86dvh, 760px)' });
|
expect(document.querySelector('.v2-access-governance-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(88dvh, 780px)' });
|
||||||
|
expect(within(governanceDialog).getByText('接入治理工作台')).toBeInTheDocument();
|
||||||
|
expect(within(governanceDialog).getByRole('list', { name: '接入治理配置摘要' })).toBeInTheDocument();
|
||||||
fireEvent.click(within(governanceDialog).getByRole('button', { name: '关闭接入治理配置' }));
|
fireEvent.click(within(governanceDialog).getByRole('button', { name: '关闭接入治理配置' }));
|
||||||
expect(governanceButton).toHaveAttribute('aria-expanded', 'false');
|
expect(governanceButton).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(document.body.style.overflow).not.toBe('hidden');
|
expect(document.body.style.overflow).not.toBe('hidden');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IconChevronDown, IconChevronRight, IconClose, IconDownload, IconMore, IconRefresh, IconSave, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
import { IconChevronDown, IconChevronRight, IconClose, IconDownload, IconMore, IconRefresh, IconSave, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, CardGroup, Collapse, Descriptions, Dropdown, Empty, Input, Select, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
import { Button, Card, CardGroup, Collapse, Descriptions, Dropdown, Empty, Input, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { FormEvent, KeyboardEvent, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { FormEvent, KeyboardEvent, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
@@ -15,6 +15,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
|
|||||||
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet';
|
||||||
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||||
import { usePlatformSession } from '../auth/AuthGate';
|
import { usePlatformSession } from '../auth/AuthGate';
|
||||||
import { canAdminister } from '../auth/session';
|
import { canAdminister } from '../auth/session';
|
||||||
@@ -356,7 +357,6 @@ export default function AccessPage() {
|
|||||||
}, [mobileLayout]);
|
}, [mobileLayout]);
|
||||||
const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN);
|
const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN);
|
||||||
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
|
||||||
useSideSheetA11y(editable && governanceOpen, '.v2-access-governance-sidesheet', 'v2-access-governance', '接入治理配置', '关闭接入治理配置');
|
|
||||||
useSideSheetA11y(mobileFiltersOpen, '.v2-access-mobile-filter-sidesheet', 'v2-access-mobile-filters', '接入车辆筛选', '关闭接入车辆筛选');
|
useSideSheetA11y(mobileFiltersOpen, '.v2-access-mobile-filter-sidesheet', 'v2-access-mobile-filters', '接入车辆筛选', '关闭接入车辆筛选');
|
||||||
const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); };
|
const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); };
|
||||||
const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); };
|
const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); };
|
||||||
@@ -505,28 +505,49 @@ export default function AccessPage() {
|
|||||||
>
|
>
|
||||||
{selected ? <VehicleInspector key={selected.vin} row={selected} onClose={() => setSelectedVIN('')} sheet mobile={mobileLayout} /> : null}
|
{selected ? <VehicleInspector key={selected.vin} row={selected} onClose={() => setSelectedVIN('')} sheet mobile={mobileLayout} /> : null}
|
||||||
</WorkspaceDetailSideSheet>
|
</WorkspaceDetailSideSheet>
|
||||||
{editable ? <SideSheet
|
{editable ? <WorkspaceSideSheet
|
||||||
className="v2-access-governance-sidesheet"
|
className="v2-access-governance-sidesheet"
|
||||||
|
variant="config"
|
||||||
visible={governanceOpen}
|
visible={governanceOpen}
|
||||||
aria-label="接入治理配置"
|
ariaLabel="接入治理配置"
|
||||||
|
closeLabel="关闭接入治理配置"
|
||||||
placement={mobileLayout ? 'bottom' : 'right'}
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
width={mobileLayout ? undefined : 560}
|
width={mobileLayout ? undefined : 560}
|
||||||
height={mobileLayout ? 'min(86dvh, 760px)' : undefined}
|
height={mobileLayout ? 'min(88dvh, 780px)' : undefined}
|
||||||
title={<div className="v2-access-sheet-title"><strong>接入治理</strong><span>集中处理待绑定来源与在线判定阈值</span></div>}
|
title="接入治理工作台"
|
||||||
|
description="核对待绑定来源,统一维护协议在线判定阈值"
|
||||||
|
badge={(unresolvedQuery.data?.total ?? 0) > 0 ? `${unresolvedQuery.data?.total.toLocaleString('zh-CN')} 项待办` : '治理正常'}
|
||||||
|
badgeColor={(unresolvedQuery.data?.total ?? 0) > 0 ? 'orange' : 'green'}
|
||||||
|
summaryItems={[
|
||||||
|
{
|
||||||
|
label: '待绑定来源',
|
||||||
|
value: (unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN'),
|
||||||
|
detail: (unresolvedQuery.data?.total ?? 0) > 0 ? '需要核对权威 VIN' : '当前没有身份待办',
|
||||||
|
tone: (unresolvedQuery.data?.total ?? 0) > 0 ? 'warning' : 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '阈值版本',
|
||||||
|
value: `v${thresholdQuery.data?.version ?? '—'}`,
|
||||||
|
detail: '在线与延迟统一口径',
|
||||||
|
tone: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '主车辆口径',
|
||||||
|
value: (summary?.totalVehicles ?? 0).toLocaleString('zh-CN'),
|
||||||
|
detail: '仅统计权威主车辆',
|
||||||
|
tone: 'neutral'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
footerNote="阈值保存后即时生效;关闭工作台不会丢失已保存配置"
|
||||||
|
primaryAction={{ label: '完成', onClick: () => setGovernanceOpen(false) }}
|
||||||
onCancel={() => setGovernanceOpen(false)}
|
onCancel={() => setGovernanceOpen(false)}
|
||||||
footer={<Button theme="solid" onClick={() => setGovernanceOpen(false)}>完成</Button>}
|
|
||||||
>
|
>
|
||||||
{governanceOpen ? <div className="v2-access-governance">
|
{governanceOpen ? <div className="v2-access-governance">
|
||||||
<Card className="v2-access-governance-summary" bodyStyle={{ padding: 0 }}>
|
|
||||||
<div className={(unresolvedQuery.data?.total ?? 0) > 0 ? 'is-attention' : ''}><small>待绑定来源</small><strong>{(unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN')}</strong><span>{(unresolvedQuery.data?.total ?? 0) > 0 ? '优先处理' : '暂无待办'}</span></div>
|
|
||||||
<div><small>阈值版本</small><strong>v{thresholdQuery.data?.version ?? '—'}</strong></div>
|
|
||||||
<div><small>主车辆口径</small><strong>{(summary?.totalVehicles ?? 0).toLocaleString('zh-CN')}</strong></div>
|
|
||||||
</Card>
|
|
||||||
{unresolvedQuery.isError ? <InlineError message={unresolvedQuery.error instanceof Error ? unresolvedQuery.error.message : '待绑定身份读取失败'} onRetry={() => unresolvedQuery.refetch()} /> : null}
|
{unresolvedQuery.isError ? <InlineError message={unresolvedQuery.error instanceof Error ? unresolvedQuery.error.message : '待绑定身份读取失败'} onRetry={() => unresolvedQuery.refetch()} /> : null}
|
||||||
<IdentityQueue items={unresolvedQuery.data?.items ?? []} total={unresolvedQuery.data?.total ?? 0} />
|
<IdentityQueue items={unresolvedQuery.data?.items ?? []} total={unresolvedQuery.data?.total ?? 0} />
|
||||||
{thresholdQuery.isError ? <InlineError message={thresholdQuery.error instanceof Error ? thresholdQuery.error.message : '接入阈值读取失败'} onRetry={() => thresholdQuery.refetch()} /> : null}
|
{thresholdQuery.isError ? <InlineError message={thresholdQuery.error instanceof Error ? thresholdQuery.error.message : '接入阈值读取失败'} onRetry={() => thresholdQuery.refetch()} /> : null}
|
||||||
<ThresholdSettings config={thresholdQuery.data} draft={thresholdDraft} editable saving={updateThreshold.isPending} error={updateThreshold.error instanceof Error ? updateThreshold.error.message : undefined} onChange={setThresholdDraft} onSave={() => thresholdDraft && updateThreshold.mutate(thresholdDraft)} />
|
<ThresholdSettings config={thresholdQuery.data} draft={thresholdDraft} editable saving={updateThreshold.isPending} error={updateThreshold.error instanceof Error ? updateThreshold.error.message : undefined} onChange={setThresholdDraft} onSave={() => thresholdDraft && updateThreshold.mutate(thresholdDraft)} />
|
||||||
</div> : null}
|
</div> : null}
|
||||||
</SideSheet> : null}
|
</WorkspaceSideSheet> : null}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,14 +92,19 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
|||||||
expect(screen.getByRole('button', { name: '刷新结果' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted');
|
expect(screen.getByRole('button', { name: '刷新结果' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted');
|
||||||
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
|
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
|
||||||
fireEvent.click(trendButton);
|
fireEvent.click(trendButton);
|
||||||
expect(screen.getByText('近 30 天趋势').closest('.semi-card')).toHaveClass('v2-reconcile-trend');
|
const trendDialog = await screen.findByRole('dialog', { name: '30 天差异趋势' });
|
||||||
expect(screen.getByText('近 30 天趋势').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
expect(trendDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-trend-sidesheet');
|
||||||
|
expect(within(trendDialog).getByText('趋势明细').closest('.semi-card')).toHaveClass('v2-reconcile-trend');
|
||||||
|
expect(within(trendDialog).getByText('趋势明细').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
||||||
|
expect(within(trendDialog).getByRole('list', { name: '30 天差异趋势摘要' })).toBeInTheDocument();
|
||||||
expect(trendButton).toHaveAttribute('aria-expanded', 'true');
|
expect(trendButton).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
fireEvent.click(within(trendDialog).getByRole('button', { name: '关闭 30 天差异趋势' }));
|
||||||
|
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间');
|
expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间');
|
||||||
expect(screen.getAllByText('2026-07-16 18:00:00').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('2026-07-16 18:00:00').length).toBeGreaterThan(0);
|
||||||
expect(document.querySelector('.v2-platform-time.v2-ops-time')).toBeInTheDocument();
|
expect(document.querySelector('.v2-platform-time.v2-ops-time')).toBeInTheDocument();
|
||||||
const issueTitles = await screen.findAllByText('多来源实时位置漂移');
|
expect((await screen.findAllByText('多来源实时位置漂移')).length).toBeGreaterThan(0);
|
||||||
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).not.toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).not.toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-reconcile-table-wrap > table')).not.toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-table-wrap > table')).not.toBeInTheDocument();
|
||||||
const reconcileRow = screen.getByTestId('reconcile-row-issue-1');
|
const reconcileRow = screen.getByTestId('reconcile-row-issue-1');
|
||||||
@@ -108,18 +113,21 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
|||||||
fireEvent.keyDown(reconcileRow, { key: ' ' });
|
fireEvent.keyDown(reconcileRow, { key: ' ' });
|
||||||
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
||||||
expect(reconcileRow).toHaveAttribute('aria-expanded', 'true');
|
expect(reconcileRow).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
const detailDialog = screen.getByRole('dialog', { name: '差异证据与处置' });
|
||||||
|
expect(detailDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-detail-sidesheet');
|
||||||
|
expect(within(detailDialog).getByRole('list', { name: '差异证据与处置摘要' })).toBeInTheDocument();
|
||||||
expect(screen.getByText('规则证据').closest('.semi-card')).toHaveClass('v2-reconcile-evidence');
|
expect(screen.getByText('规则证据').closest('.semi-card')).toHaveClass('v2-reconcile-evidence');
|
||||||
expect(screen.getByText('复核结论').closest('.semi-card')).toHaveClass('v2-reconcile-review');
|
expect(screen.getByText('复核结论').closest('.semi-card')).toHaveClass('v2-reconcile-review');
|
||||||
expect(screen.getByText('处理履历').closest('.semi-card')).toHaveClass('v2-reconcile-actions');
|
expect(screen.getByText('处理履历').closest('.semi-card')).toHaveClass('v2-reconcile-actions');
|
||||||
expect(screen.getByText('规则证据').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
expect(screen.getByText('规则证据').closest('.v2-workspace-panel-header')).toHaveClass('is-compact');
|
||||||
expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('复核提示')).toBeInTheDocument();
|
expect(screen.getByText('复核提示')).toBeInTheDocument();
|
||||||
expect(screen.getByText('坐标差距')).toBeInTheDocument();
|
expect(screen.getByText('坐标差距')).toBeInTheDocument();
|
||||||
expect(screen.getByText('distanceM')).toBeInTheDocument();
|
expect(screen.getByText('distanceM')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1,286 m')).toBeInTheDocument();
|
expect(screen.getByText('1,286 m')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' }));
|
fireEvent.click(within(detailDialog).getByRole('button', { name: '关闭差异证据与处置' }));
|
||||||
expect(reconcileRow).toHaveAttribute('aria-expanded', 'false');
|
expect(reconcileRow).toHaveAttribute('aria-expanded', 'false');
|
||||||
fireEvent.click(issueTitles[0]);
|
fireEvent.click(reconcileRow);
|
||||||
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
expect(await screen.findByText('规则证据')).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('radio', { name: '采信 GB32960' }));
|
fireEvent.click(screen.getByRole('radio', { name: '采信 GB32960' }));
|
||||||
fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } });
|
fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } });
|
||||||
@@ -152,10 +160,11 @@ test('renders only the compact mobile reconciliation surface and defers secondar
|
|||||||
expect(await screen.findByRole('dialog', { name: '差异证据与处置' })).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '差异证据与处置' })).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet')).toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet')).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||||
expect(document.querySelector('.v2-reconcile-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' });
|
expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet');
|
||||||
expect(await screen.findByRole('button', { name: '关闭差异详情' })).toBeInTheDocument();
|
expect(document.querySelector('.v2-reconcile-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(88dvh, 780px)' });
|
||||||
|
expect(await screen.findByRole('button', { name: '关闭差异证据与处置' })).toBeInTheDocument();
|
||||||
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'true');
|
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'true');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' }));
|
fireEvent.click(screen.getByRole('button', { name: '关闭差异证据与处置' }));
|
||||||
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false');
|
expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
const toolbar = document.querySelector('.v2-reconcile-toolbar');
|
const toolbar = document.querySelector('.v2-reconcile-toolbar');
|
||||||
@@ -166,14 +175,13 @@ test('renders only the compact mobile reconciliation surface and defers secondar
|
|||||||
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: '30 天趋势' }));
|
fireEvent.click(screen.getByRole('button', { name: '30 天趋势' }));
|
||||||
expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument();
|
expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
|
||||||
expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(62dvh, 560px)' });
|
expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' });
|
||||||
const trend = screen.getByText('近 30 天趋势').closest('.v2-reconcile-trend');
|
const trend = screen.getByText('趋势明细').closest('.v2-reconcile-trend');
|
||||||
expect(trend).toBeInTheDocument();
|
expect(trend).toBeInTheDocument();
|
||||||
expect(screen.getByText(/最近运行/)).toBeInTheDocument();
|
expect(screen.getByText(/最近运行/)).toBeInTheDocument();
|
||||||
expect(document.querySelector('.v2-reconcile-trend-toggle')).toHaveAttribute('aria-expanded', 'true');
|
fireEvent.click(screen.getByRole('button', { name: '关闭 30 天差异趋势' }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: '收起趋势' }));
|
expect(screen.queryByText('趋势明细')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reconciles service identities with bound and identity-required vehicles', async () => {
|
test('reconciles service identities with bound and identity-required vehicles', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IconAlertTriangle, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
import { IconAlertTriangle, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { Button, Card, Empty, Input, RadioGroup, Select, SideSheet, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
import { Button, Card, Empty, Input, RadioGroup, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
|
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
@@ -9,10 +9,10 @@ import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
|||||||
import { PlatformTime } from '../shared/PlatformTime';
|
import { PlatformTime } from '../shared/PlatformTime';
|
||||||
import { TablePagination } from '../shared/TablePagination';
|
import { TablePagination } from '../shared/TablePagination';
|
||||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||||
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
import { detailTriggerRow } from '../shared/detailTriggerRow';
|
||||||
import { QUERY_MEMORY } from '../queryPolicy';
|
import { QUERY_MEMORY } from '../queryPolicy';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
|
||||||
|
|
||||||
const DESKTOP_PAGE_SIZE = 50;
|
const DESKTOP_PAGE_SIZE = 50;
|
||||||
const MOBILE_PAGE_SIZE = 20;
|
const MOBILE_PAGE_SIZE = 20;
|
||||||
@@ -171,27 +171,16 @@ function reviewGuidance(issue: ReconciliationIssue) {
|
|||||||
}[issue.ruleCode] ?? '先核对原始证据、影响对象和时间范围,再选择结论并留下可追溯说明。';
|
}[issue.ruleCode] ?? '先核对原始证据、影响对象和时间范围,再选择结论并留下可追溯说明。';
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReconciliationTrend({ data, maxTrend, onClose }: {
|
function ReconciliationTrend({ data, maxTrend }: {
|
||||||
data?: ReconciliationSummary;
|
data?: ReconciliationSummary;
|
||||||
maxTrend: number;
|
maxTrend: number;
|
||||||
onClose: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
return <Card className="v2-reconcile-trend" bodyStyle={{ padding: 0 }}>
|
return <Card className="v2-reconcile-trend" bodyStyle={{ padding: 0 }}>
|
||||||
<WorkspacePanelHeader
|
<WorkspacePanelHeader
|
||||||
variant="compact"
|
variant="compact"
|
||||||
title="近 30 天趋势"
|
title="趋势明细"
|
||||||
|
description="最近 14 次自动对账运行的存量、新增与恢复"
|
||||||
meta={<span>最近运行 <PlatformTime className="v2-ops-time" value={data?.lastRunAt} sourceZone="utc" /></span>}
|
meta={<span>最近运行 <PlatformTime className="v2-ops-time" value={data?.lastRunAt} sourceZone="utc" /></span>}
|
||||||
actions={<Button
|
|
||||||
className="v2-reconcile-trend-toggle"
|
|
||||||
theme="borderless"
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
icon={<IconChevronUp />}
|
|
||||||
aria-label="收起趋势"
|
|
||||||
aria-expanded
|
|
||||||
aria-controls="v2-reconcile-trend"
|
|
||||||
onClick={onClose}
|
|
||||||
>收起</Button>}
|
|
||||||
/>
|
/>
|
||||||
<div>{data?.trend.slice(-14).map((item) => <article key={item.date}><time>{item.date.slice(5)}</time><div title={`存量 ${item.active},新增 ${item.new},恢复 ${item.recovered}`}><i className="is-active" style={{ width: `${Math.max(2, item.active / maxTrend * 100)}%` }} /><i className="is-new" style={{ width: `${Math.max(0, item.new / maxTrend * 100)}%` }} /><i className="is-recovered" style={{ width: `${Math.max(0, item.recovered / maxTrend * 100)}%` }} /></div><b>{item.active}</b></article>)}</div>
|
<div>{data?.trend.slice(-14).map((item) => <article key={item.date}><time>{item.date.slice(5)}</time><div title={`存量 ${item.active},新增 ${item.new},恢复 ${item.recovered}`}><i className="is-active" style={{ width: `${Math.max(2, item.active / maxTrend * 100)}%` }} /><i className="is-new" style={{ width: `${Math.max(0, item.new / maxTrend * 100)}%` }} /><i className="is-recovered" style={{ width: `${Math.max(0, item.recovered / maxTrend * 100)}%` }} /></div><b>{item.active}</b></article>)}</div>
|
||||||
{!data?.trend.length ? <p>完成首次每日检测后显示趋势。</p> : null}
|
{!data?.trend.length ? <p>完成首次每日检测后显示趋势。</p> : null}
|
||||||
@@ -221,19 +210,19 @@ function ReconciliationDetail({ issue, onClose, sheet = false }: { issue: Reconc
|
|||||||
const evidenceEntries = Object.entries(issue.evidence ?? {});
|
const evidenceEntries = Object.entries(issue.evidence ?? {});
|
||||||
const statusOptions = reviewStatusOptions(issue);
|
const statusOptions = reviewStatusOptions(issue);
|
||||||
return <Card className={`v2-reconcile-detail${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }} aria-label="差异证据与处置">
|
return <Card className={`v2-reconcile-detail${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }} aria-label="差异证据与处置">
|
||||||
<WorkspacePanelHeader
|
{!sheet ? <WorkspacePanelHeader
|
||||||
className="v2-reconcile-detail-heading"
|
className="v2-reconcile-detail-heading"
|
||||||
title={issue.title}
|
title={issue.title}
|
||||||
description={ruleLabel(issue.ruleCode)}
|
description={ruleLabel(issue.ruleCode)}
|
||||||
meta={<span className="v2-reconcile-detail-status"><ReconciliationSeverityTag severity={issue.severity} /><ReconciliationStatusTag status={issue.status} /></span>}
|
meta={<span className="v2-reconcile-detail-status"><ReconciliationSeverityTag severity={issue.severity} /><ReconciliationStatusTag status={issue.status} /></span>}
|
||||||
actions={sheet ? undefined : <Button theme="borderless" type="tertiary" icon={<IconClose />} onClick={onClose} aria-label="关闭差异详情" />}
|
actions={<Button theme="borderless" type="tertiary" icon={<IconClose />} onClick={onClose} aria-label="关闭差异详情" />}
|
||||||
/>
|
/> : null}
|
||||||
<div className="v2-reconcile-detail-body">
|
<div className="v2-reconcile-detail-body">
|
||||||
<section className="v2-reconcile-identity">
|
{!sheet ? <section className="v2-reconcile-identity">
|
||||||
<div><small>车辆</small><strong>{issue.plate || '未登记车牌'}</strong><span>{issue.vin || '非单车差异'}</span></div>
|
<div><small>车辆</small><strong>{issue.plate || '未登记车牌'}</strong><span>{issue.vin || '非单车差异'}</span></div>
|
||||||
<div><small>来源</small><strong>{[issue.protocolA, issue.protocolB].filter(Boolean).join(' ↔ ') || '平台口径'}</strong><span>累计发现 {issue.occurrenceCount.toLocaleString('zh-CN')} 次</span></div>
|
<div><small>来源</small><strong>{[issue.protocolA, issue.protocolB].filter(Boolean).join(' ↔ ') || '平台口径'}</strong><span>累计发现 {issue.occurrenceCount.toLocaleString('zh-CN')} 次</span></div>
|
||||||
<div><small>时间</small><strong><PlatformTime className="v2-ops-time" value={issue.lastSeenAt} sourceZone="utc" /></strong><span>首次 <PlatformTime className="v2-ops-time" value={issue.firstSeenAt} sourceZone="utc" /></span></div>
|
<div><small>时间</small><strong><PlatformTime className="v2-ops-time" value={issue.lastSeenAt} sourceZone="utc" /></strong><span>首次 <PlatformTime className="v2-ops-time" value={issue.firstSeenAt} sourceZone="utc" /></span></div>
|
||||||
</section>
|
</section> : null}
|
||||||
<section className="v2-reconcile-decision-guide">
|
<section className="v2-reconcile-decision-guide">
|
||||||
<IconAlertTriangle />
|
<IconAlertTriangle />
|
||||||
<div><strong>复核提示</strong><p>{reviewGuidance(issue)}</p></div>
|
<div><strong>复核提示</strong><p>{reviewGuidance(issue)}</p></div>
|
||||||
@@ -279,8 +268,14 @@ export default function ReconciliationCenter() {
|
|||||||
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
const [filtersCollapsed, setFiltersCollapsed] = useState(true);
|
||||||
const [trendExpanded, setTrendExpanded] = useState(false);
|
const [trendExpanded, setTrendExpanded] = useState(false);
|
||||||
const closeDetail = () => setSelectedID('');
|
const closeDetail = () => setSelectedID('');
|
||||||
useSideSheetA11y(mobileLayout && Boolean(selectedID), '.v2-reconcile-detail-sidesheet', 'v2-reconcile-detail-sheet', '差异证据与处置', '关闭差异详情');
|
const openDetail = (id: string) => {
|
||||||
useSideSheetA11y(mobileLayout && trendExpanded, '.v2-reconcile-trend-sidesheet', 'v2-reconcile-trend-sheet', '30 天差异趋势', '关闭 30 天差异趋势');
|
setTrendExpanded(false);
|
||||||
|
setSelectedID(id);
|
||||||
|
};
|
||||||
|
const toggleTrend = () => {
|
||||||
|
setSelectedID('');
|
||||||
|
setTrendExpanded((value) => !value);
|
||||||
|
};
|
||||||
useEffect(() => setOffset(0), [deferredKeyword, mobileLayout, ruleCode, severity, status]);
|
useEffect(() => setOffset(0), [deferredKeyword, mobileLayout, ruleCode, severity, status]);
|
||||||
|
|
||||||
const summary = useQuery({
|
const summary = useQuery({
|
||||||
@@ -320,11 +315,11 @@ export default function ReconciliationCenter() {
|
|||||||
const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)}${activeFilterCount ? ` · 另 ${activeFilterCount} 项` : ''}`;
|
const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)}${activeFilterCount ? ` · 另 ${activeFilterCount} 项` : ''}`;
|
||||||
const selectedIssue = detail.data ?? issueRows.find((item) => item.id === selectedID);
|
const selectedIssue = detail.data ?? issueRows.find((item) => item.id === selectedID);
|
||||||
const detailPanel = selectedID && detail.isPending
|
const detailPanel = selectedID && detail.isPending
|
||||||
? <Card className={`v2-reconcile-detail${mobileLayout ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}><div className="v2-reconcile-detail-loading" role="status"><Spin size="middle" tip="正在读取差异证据…" /></div></Card>
|
? <Card className="v2-reconcile-detail is-sheet" bodyStyle={{ padding: 0 }}><div className="v2-reconcile-detail-loading" role="status"><Spin size="middle" tip="正在读取差异证据…" /></div></Card>
|
||||||
: selectedID && detail.isError
|
: selectedID && detail.isError
|
||||||
? <Card className={`v2-reconcile-detail${mobileLayout ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}><InlineError message={detail.error.message} onRetry={() => detail.refetch()} /></Card>
|
? <Card className="v2-reconcile-detail is-sheet" bodyStyle={{ padding: 0 }}><InlineError message={detail.error.message} onRetry={() => detail.refetch()} /></Card>
|
||||||
: detail.data
|
: detail.data
|
||||||
? <ReconciliationDetail issue={detail.data} onClose={closeDetail} sheet={mobileLayout} />
|
? <ReconciliationDetail issue={detail.data} onClose={closeDetail} sheet />
|
||||||
: null;
|
: null;
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '问题 / 等级', dataIndex: 'title', width: 286, render: (_value: string, item: ReconciliationIssue) => <span className="v2-reconcile-cell v2-reconcile-issue-cell"><span><strong>{item.title}</strong><ReconciliationSeverityTag severity={item.severity} /></span><small>{ruleLabel(item.ruleCode)} · 命中 {item.occurrenceCount.toLocaleString('zh-CN')} 次</small></span> },
|
{ title: '问题 / 等级', dataIndex: 'title', width: 286, render: (_value: string, item: ReconciliationIssue) => <span className="v2-reconcile-cell v2-reconcile-issue-cell"><span><strong>{item.title}</strong><ReconciliationSeverityTag severity={item.severity} /></span><small>{ruleLabel(item.ruleCode)} · 命中 {item.occurrenceCount.toLocaleString('zh-CN')} 次</small></span> },
|
||||||
@@ -349,7 +344,7 @@ export default function ReconciliationCenter() {
|
|||||||
aria-label="30 天趋势"
|
aria-label="30 天趋势"
|
||||||
aria-expanded={trendExpanded}
|
aria-expanded={trendExpanded}
|
||||||
aria-controls="v2-reconcile-trend"
|
aria-controls="v2-reconcile-trend"
|
||||||
onClick={() => setTrendExpanded((value) => !value)}
|
onClick={toggleTrend}
|
||||||
>30 天趋势</Button>
|
>30 天趋势</Button>
|
||||||
<Button className="v2-workspace-mobile-tool-button is-muted" theme="light" icon={<IconRefresh />} aria-label="刷新结果" loading={summary.isFetching || issues.isFetching} onClick={refresh}>刷新结果</Button>
|
<Button className="v2-workspace-mobile-tool-button is-muted" theme="light" icon={<IconRefresh />} aria-label="刷新结果" loading={summary.isFetching || issues.isFetching} onClick={refresh}>刷新结果</Button>
|
||||||
</>}
|
</>}
|
||||||
@@ -363,7 +358,7 @@ export default function ReconciliationCenter() {
|
|||||||
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>已自动恢复</small><strong>{data?.recovered.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>
|
||||||
</div>
|
</div>
|
||||||
<div className={`v2-reconcile-layout${trendExpanded ? ' is-trend-open' : ''}`}>
|
<div className="v2-reconcile-layout">
|
||||||
<div className="v2-reconcile-main">
|
<div className="v2-reconcile-main">
|
||||||
<MobileFilterToggle
|
<MobileFilterToggle
|
||||||
title="筛选差异"
|
title="筛选差异"
|
||||||
@@ -408,9 +403,9 @@ export default function ReconciliationCenter() {
|
|||||||
expanded: selectedID === item.id,
|
expanded: selectedID === item.id,
|
||||||
label: `查看 ${item.plate || item.title} 差异证据`,
|
label: `查看 ${item.plate || item.title} 差异证据`,
|
||||||
testId: `reconcile-row-${item.id}`,
|
testId: `reconcile-row-${item.id}`,
|
||||||
onOpen: () => setSelectedID(item.id)
|
onOpen: () => openDetail(item.id)
|
||||||
}) : ({})} /> : <div className="v2-reconcile-mobile-list">{issueRows.map((item) => <Card key={item.id} className={`v2-reconcile-mobile-card${selectedID === item.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}>
|
}) : ({})} /> : <div className="v2-reconcile-mobile-list">{issueRows.map((item) => <Card key={item.id} className={`v2-reconcile-mobile-card${selectedID === item.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}>
|
||||||
<Button theme="borderless" type="tertiary" className="v2-reconcile-mobile-action" aria-pressed={selectedID === item.id} aria-expanded={selectedID === item.id} aria-label={`查看 ${item.plate || item.title} 差异证据`} onClick={() => setSelectedID(item.id)}>
|
<Button theme="borderless" type="tertiary" className="v2-reconcile-mobile-action" aria-pressed={selectedID === item.id} aria-expanded={selectedID === item.id} aria-label={`查看 ${item.plate || item.title} 差异证据`} onClick={() => openDetail(item.id)}>
|
||||||
<span className="v2-reconcile-mobile-content">
|
<span className="v2-reconcile-mobile-content">
|
||||||
<header><strong>{item.plate || '平台级差异'}</strong><span><ReconciliationSeverityTag severity={item.severity} /><ReconciliationStatusTag status={item.status} /></span></header>
|
<header><strong>{item.plate || '平台级差异'}</strong><span><ReconciliationSeverityTag severity={item.severity} /><ReconciliationStatusTag status={item.status} /></span></header>
|
||||||
<p>{item.title}</p>
|
<p>{item.title}</p>
|
||||||
@@ -424,32 +419,81 @@ export default function ReconciliationCenter() {
|
|||||||
</div>
|
</div>
|
||||||
<footer className="v2-reconcile-pagination"><TablePagination page={currentPage} totalPages={totalPages} info={`共 ${(page?.total ?? 0).toLocaleString('zh-CN')} 条 · 本页 ${activeCount} 条活跃`} onPageChange={(next) => setOffset((next - 1) * pageSize)} /></footer>
|
<footer className="v2-reconcile-pagination"><TablePagination page={currentPage} totalPages={totalPages} info={`共 ${(page?.total ?? 0).toLocaleString('zh-CN')} 条 · 本页 ${activeCount} 条活跃`} onPageChange={(next) => setOffset((next - 1) * pageSize)} /></footer>
|
||||||
</div>
|
</div>
|
||||||
{trendExpanded && !mobileLayout ? <div id="v2-reconcile-trend" className="v2-reconcile-trend-slot"><ReconciliationTrend data={data} maxTrend={maxTrend} onClose={() => setTrendExpanded(false)} /></div> : null}
|
|
||||||
{!mobileLayout ? detailPanel : null}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<SideSheet
|
<WorkspaceSideSheet
|
||||||
className="v2-reconcile-detail-sidesheet"
|
className="v2-reconcile-detail-sidesheet"
|
||||||
visible={mobileLayout && Boolean(selectedID)}
|
variant="detail"
|
||||||
placement="bottom"
|
visible={Boolean(selectedID)}
|
||||||
height="min(84dvh, 740px)"
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
aria-label="差异证据与处置"
|
width={mobileLayout ? undefined : 680}
|
||||||
title={<div className="v2-reconcile-sheet-title"><strong>差异证据与处置</strong><span>{selectedIssue ? `${selectedIssue.plate || '平台级差异'} · ${ruleLabel(selectedIssue.ruleCode)}` : '加载规则证据与处置履历'}</span></div>}
|
height={mobileLayout ? 'min(88dvh, 780px)' : undefined}
|
||||||
|
ariaLabel="差异证据与处置"
|
||||||
|
closeLabel="关闭差异证据与处置"
|
||||||
|
title={selectedIssue?.title ?? '差异证据与处置'}
|
||||||
|
description={selectedIssue ? `${ruleLabel(selectedIssue.ruleCode)} · 原始证据、复核结论与处理履历` : '正在加载规则证据与处置履历'}
|
||||||
|
badge={selectedIssue ? `${severityLabel(selectedIssue.severity)} · ${statusLabel(selectedIssue.status)}` : '读取中'}
|
||||||
|
badgeColor={selectedIssue?.severity === 'critical' ? 'red' : selectedIssue?.severity === 'major' ? 'orange' : 'grey'}
|
||||||
|
summaryItems={selectedIssue ? [
|
||||||
|
{
|
||||||
|
label: '车辆',
|
||||||
|
value: selectedIssue.plate || '平台级差异',
|
||||||
|
detail: selectedIssue.vin || '非单车口径',
|
||||||
|
tone: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '证据来源',
|
||||||
|
value: [selectedIssue.protocolA, selectedIssue.protocolB].filter(Boolean).join(' ↔ ') || '平台口径',
|
||||||
|
detail: `累计发现 ${selectedIssue.occurrenceCount.toLocaleString('zh-CN')} 次`,
|
||||||
|
tone: selectedIssue.severity === 'critical' ? 'danger' : 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '最近发现',
|
||||||
|
value: <PlatformTime className="v2-ops-time" value={selectedIssue.lastSeenAt} sourceZone="utc" />,
|
||||||
|
detail: <span>首次 <PlatformTime className="v2-ops-time" value={selectedIssue.firstSeenAt} sourceZone="utc" /></span>,
|
||||||
|
tone: 'neutral'
|
||||||
|
}
|
||||||
|
] : []}
|
||||||
onCancel={closeDetail}
|
onCancel={closeDetail}
|
||||||
>
|
>
|
||||||
{mobileLayout ? detailPanel : null}
|
{detailPanel}
|
||||||
</SideSheet>
|
</WorkspaceSideSheet>
|
||||||
<SideSheet
|
<WorkspaceSideSheet
|
||||||
className="v2-reconcile-trend-sidesheet"
|
className="v2-reconcile-trend-sidesheet"
|
||||||
visible={mobileLayout && trendExpanded}
|
variant="detail"
|
||||||
placement="bottom"
|
visible={trendExpanded}
|
||||||
height="min(62dvh, 560px)"
|
placement={mobileLayout ? 'bottom' : 'right'}
|
||||||
aria-label="30 天差异趋势"
|
width={mobileLayout ? undefined : 520}
|
||||||
|
height={mobileLayout ? 'min(72dvh, 620px)' : undefined}
|
||||||
|
ariaLabel="30 天差异趋势"
|
||||||
|
closeLabel="关闭 30 天差异趋势"
|
||||||
title="30 天差异趋势"
|
title="30 天差异趋势"
|
||||||
footer={null}
|
description="查看自动对账的差异存量、新增与恢复走势"
|
||||||
|
badge="每日自动对账"
|
||||||
|
badgeColor="blue"
|
||||||
|
summaryItems={[
|
||||||
|
{
|
||||||
|
label: '活跃差异',
|
||||||
|
value: data?.active.toLocaleString('zh-CN') ?? '—',
|
||||||
|
detail: `${data?.pending.toLocaleString('zh-CN') ?? '—'} 项待复核`,
|
||||||
|
tone: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'SLA 超时',
|
||||||
|
value: data?.overSla.toLocaleString('zh-CN') ?? '—',
|
||||||
|
detail: '超过 24 小时',
|
||||||
|
tone: data?.overSla ? 'danger' : 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已自动恢复',
|
||||||
|
value: data?.recovered.toLocaleString('zh-CN') ?? '—',
|
||||||
|
detail: '规则已不再命中',
|
||||||
|
tone: 'success'
|
||||||
|
}
|
||||||
|
]}
|
||||||
onCancel={() => setTrendExpanded(false)}
|
onCancel={() => setTrendExpanded(false)}
|
||||||
>
|
>
|
||||||
{mobileLayout && trendExpanded ? <div id="v2-reconcile-trend"><ReconciliationTrend data={data} maxTrend={maxTrend} onClose={() => setTrendExpanded(false)} /></div> : null}
|
{trendExpanded ? <div id="v2-reconcile-trend"><ReconciliationTrend data={data} maxTrend={maxTrend} /></div> : null}
|
||||||
</SideSheet>
|
</WorkspaceSideSheet>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.AccessPage).toContain('<Card className={`v2-access-inspector-v3');
|
expect(corePageSources.AccessPage).toContain('<Card className={`v2-access-inspector-v3');
|
||||||
expect(corePageSources.AccessPage).toContain('className="v2-access-detail-sidesheet"');
|
expect(corePageSources.AccessPage).toContain('className="v2-access-detail-sidesheet"');
|
||||||
expect(corePageSources.AccessPage).toContain('className="v2-access-governance-sidesheet"');
|
expect(corePageSources.AccessPage).toContain('className="v2-access-governance-sidesheet"');
|
||||||
|
expect(corePageSources.AccessPage).toContain('<WorkspaceSideSheet');
|
||||||
|
expect(corePageSources.AccessPage).toContain('variant="config"');
|
||||||
expect(corePageSources.AccessPage).toContain('className="v2-access-filter-card-v3"');
|
expect(corePageSources.AccessPage).toContain('className="v2-access-filter-card-v3"');
|
||||||
expect(corePageSources.AccessPage).not.toContain('<section className="v2-access-table-v3"');
|
expect(corePageSources.AccessPage).not.toContain('<section className="v2-access-table-v3"');
|
||||||
expect(corePageSources.AccessPage).not.toContain('<aside className="v2-access-inspector-v3"');
|
expect(corePageSources.AccessPage).not.toContain('<aside className="v2-access-inspector-v3"');
|
||||||
@@ -479,8 +481,10 @@ describe('V2 production entry', () => {
|
|||||||
expect(reconciliationSource).toContain('<Card className="v2-reconcile-kpi-card');
|
expect(reconciliationSource).toContain('<Card className="v2-reconcile-kpi-card');
|
||||||
expect(reconciliationSource).toContain('v2-reconcile-trend');
|
expect(reconciliationSource).toContain('v2-reconcile-trend');
|
||||||
expect(reconciliationSource).toContain('<Card className={`v2-reconcile-detail');
|
expect(reconciliationSource).toContain('<Card className={`v2-reconcile-detail');
|
||||||
expect(reconciliationSource).toContain('<SideSheet');
|
expect(reconciliationSource).toContain('<WorkspaceSideSheet');
|
||||||
|
expect(reconciliationSource).toContain('variant="detail"');
|
||||||
expect(reconciliationSource).toContain('className="v2-reconcile-detail-sidesheet"');
|
expect(reconciliationSource).toContain('className="v2-reconcile-detail-sidesheet"');
|
||||||
|
expect(reconciliationSource).not.toContain('<SideSheet');
|
||||||
expect(reconciliationSource).toContain('<Card className="v2-reconcile-evidence"');
|
expect(reconciliationSource).toContain('<Card className="v2-reconcile-evidence"');
|
||||||
expect(reconciliationSource).toContain("from '../shared/WorkspacePanelHeader'");
|
expect(reconciliationSource).toContain("from '../shared/WorkspacePanelHeader'");
|
||||||
expect(reconciliationSource).toContain('<WorkspacePanelHeader');
|
expect(reconciliationSource).toContain('<WorkspacePanelHeader');
|
||||||
|
|||||||
@@ -21686,3 +21686,94 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Semi UI convergence: access governance and reconciliation evidence.
|
||||||
|
* Keep the queue/table stable while configuration, evidence and trends share
|
||||||
|
* the same identity, summary and scroll hierarchy.
|
||||||
|
*/
|
||||||
|
.v2-access-governance-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-governance-sidesheet .v2-access-governance {
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-governance-sidesheet .v2-access-identity-queue-v3,
|
||||||
|
.v2-access-governance-sidesheet .v2-access-settings {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 7px 22px rgba(31, 53, 80, .045);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:first-child strong,
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:nth-child(2) strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child strong,
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child em {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet.semi-card {
|
||||||
|
min-height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet > .semi-card-body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-reconcile-detail-body {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-trend-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-trend-sidesheet .v2-workspace-config-content > #v2-reconcile-trend,
|
||||||
|
.v2-reconcile-trend-sidesheet .v2-reconcile-trend.semi-card {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-trend-sidesheet .v2-reconcile-trend.semi-card {
|
||||||
|
border: 1px solid #dce5ef;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 7px 22px rgba(31, 53, 80, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-access-governance-sidesheet .v2-workspace-config-content,
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-reconcile-detail-body,
|
||||||
|
.v2-reconcile-trend-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:first-child strong,
|
||||||
|
.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:nth-child(2) strong {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
|
||||||
|
.v2-access-governance-sidesheet .v2-access-governance {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-governance-sidesheet .v2-access-identity-queue-v3,
|
||||||
|
.v2-access-governance-sidesheet .v2-access-settings {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user