refine Semi UI history evidence workspace
This commit is contained in:
@@ -136,6 +136,20 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang
|
||||
expect(screen.queryByRole('heading', { level: 5, name: '导出任务' })).not.toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-data-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-history-table-scroll > table')).not.toBeInTheDocument();
|
||||
const desktopHistoryScroll = view.container.querySelector<HTMLElement>('.v2-history-table-scroll');
|
||||
expect(desktopHistoryScroll).toHaveAttribute('tabindex', '0');
|
||||
expect(desktopHistoryScroll).toHaveAttribute('aria-label', '历史明细表格,可使用左右方向键浏览业务字段');
|
||||
const desktopTableScroller = desktopHistoryScroll?.querySelector<HTMLElement>('.semi-table-body');
|
||||
expect(desktopTableScroller).toBeInTheDocument();
|
||||
const scrollBy = vi.fn();
|
||||
Object.defineProperty(desktopTableScroller!, 'scrollBy', { configurable: true, value: scrollBy });
|
||||
fireEvent.keyDown(desktopHistoryScroll!, { key: 'ArrowRight' });
|
||||
expect(scrollBy).toHaveBeenCalledWith({ left: 260, behavior: 'smooth' });
|
||||
fireEvent.keyDown(desktopHistoryScroll!, { key: 'ArrowLeft' });
|
||||
expect(scrollBy).toHaveBeenCalledWith({ left: -260, behavior: 'smooth' });
|
||||
expect(screen.getByRole('columnheader', { name: '设备时间' })).toHaveClass('semi-table-cell-fixed-left', 'v2-history-device-time-column');
|
||||
expect(screen.getByRole('columnheader', { name: '车牌' })).toHaveClass('semi-table-cell-fixed-left', 'v2-history-plate-column');
|
||||
expect(view.container.querySelector('th.v2-history-selection-column')).toHaveClass('semi-table-cell-fixed-left');
|
||||
expect(view.container.querySelector('.v2-history-mobile-list')).not.toBeInTheDocument();
|
||||
const desktopHistoryRow = screen.getByTestId('history-row-OLDVIN-row');
|
||||
expect(desktopHistoryRow).toHaveAttribute('role', 'button');
|
||||
@@ -205,6 +219,8 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
||||
expect(action).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(await screen.findByRole('dialog', { name: '历史数据详情' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-history-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||
expect(document.querySelector('.v2-history-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 720px)' });
|
||||
expect(await screen.findByText('MOBILEVIN-evidence')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据详情' }));
|
||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||
@@ -215,12 +231,22 @@ test('renders only selectable Semi history cards on mobile', async () => {
|
||||
const exportJobsButton = screen.getByRole('button', { name: '查看导出任务' });
|
||||
fireEvent.click(exportJobsButton);
|
||||
expect(await screen.findByRole('dialog', { name: '历史数据导出任务' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-history-export-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||
expect(document.querySelector('.v2-history-export-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(76dvh, 680px)' });
|
||||
await waitFor(() => expect(mocks.historyExports).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByText('暂无导出任务')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭历史数据导出任务' }));
|
||||
expect(exportJobsButton).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
expect(screen.queryByText('暂无导出任务')).not.toBeInTheDocument();
|
||||
|
||||
const columnSettingsButton = screen.getByRole('button', { name: '列设置' });
|
||||
fireEvent.click(columnSettingsButton);
|
||||
expect(await screen.findByRole('dialog', { name: '列显示设置' })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-history-column-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||
expect(document.querySelector('.v2-history-column-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(68dvh, 580px)' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭列显示设置' }));
|
||||
expect(columnSettingsButton).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('releases export job state immediately after leaving the history page', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, Progress, Select, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import type { HistoryDataResponse, HistoryDataRow, HistoryExportRequest, HistoryMetricDefinition, HistorySeriesResponse } from '../../api/types';
|
||||
@@ -161,6 +161,7 @@ function ColumnVisibilityPanel({ metrics, visible, visibleKeys, onToggle, onShow
|
||||
onReset: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const mobileLayout = useMobileLayout();
|
||||
const [search, setSearch] = useState('');
|
||||
const filteredMetrics = useMemo(() => {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
@@ -173,7 +174,9 @@ function ColumnVisibilityPanel({ metrics, visible, visibleKeys, onToggle, onShow
|
||||
className="v2-history-column-sidesheet"
|
||||
visible={visible}
|
||||
aria-label="列显示设置"
|
||||
width={460}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 460}
|
||||
height={mobileLayout ? 'min(68dvh, 580px)' : undefined}
|
||||
title={<div className="v2-history-column-title"><strong>列显示设置</strong><span>基础身份与时间列保持显示</span></div>}
|
||||
onCancel={onClose}
|
||||
footer={<div className="v2-history-column-footer"><span>已选 {visibleKeys.length} / {metrics.length}</span><Button theme="light" onClick={onShowAll} disabled={!metrics.length}>显示全部</Button><Button theme="light" onClick={onReset} disabled={!metrics.length}>恢复默认</Button></div>}
|
||||
@@ -188,12 +191,12 @@ function ColumnVisibilityPanel({ metrics, visible, visibleKeys, onToggle, onShow
|
||||
function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: HistoryDataRow[]; metrics: HistoryMetricDefinition[]; selectedRowID?: string; onSelect: (row: HistoryDataRow) => void }) {
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '', dataIndex: 'selection', width: 42,
|
||||
title: '', dataIndex: 'selection', width: 42, fixed: 'left' as const, className: 'v2-history-selection-column',
|
||||
render: (_: unknown, row: HistoryDataRow) => <Checkbox checked={selectedRowID === row.id} onChange={() => onSelect(row)} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} />
|
||||
},
|
||||
{ title: '设备时间', dataIndex: 'deviceTime', width: 150, render: (value: string) => <PlatformTime className="v2-history-time" value={value} /> },
|
||||
{ title: '设备时间', dataIndex: 'deviceTime', width: 150, fixed: 'left' as const, className: 'v2-history-device-time-column', render: (value: string) => <PlatformTime className="v2-history-time" value={value} /> },
|
||||
{ title: '车牌', dataIndex: 'plate', width: 110, fixed: 'left' as const, className: 'v2-history-plate-column', render: (value: string) => value || '—' },
|
||||
{ title: '服务时间', dataIndex: 'serverTime', width: 150, render: (value: string) => <PlatformTime className="v2-history-time" value={value} /> },
|
||||
{ title: '车牌', dataIndex: 'plate', width: 110, render: (value: string) => value || '—' },
|
||||
{ title: 'VIN', dataIndex: 'vin', width: 160, render: (value: string) => <span className="v2-history-vin" title={value}>{value}</span> },
|
||||
{ title: '协议', dataIndex: 'protocol', width: 100 },
|
||||
...metrics.map((metric) => ({
|
||||
@@ -220,7 +223,8 @@ function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: Hi
|
||||
|
||||
return <Table
|
||||
className="v2-history-data-table"
|
||||
style={{ minWidth }}
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: minWidth }}
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey="id"
|
||||
@@ -333,6 +337,13 @@ export default function HistoryPage() {
|
||||
return { ...current, [criteria.category]: next };
|
||||
});
|
||||
const setVisibleMetrics = (keys: string[]) => setVisibleByCategory((current) => ({ ...current, [criteria.category]: keys }));
|
||||
const scrollHistoryTable = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (mobileLayout || event.currentTarget !== event.target || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;
|
||||
const tableScroller = event.currentTarget.querySelector<HTMLElement>('.semi-table-body');
|
||||
if (!tableScroller) return;
|
||||
event.preventDefault();
|
||||
tableScroller.scrollBy({ left: event.key === 'ArrowRight' ? 260 : -260, behavior: 'smooth' });
|
||||
};
|
||||
const totalPages = Math.max(1, Math.ceil((result?.total ?? 0) / limit));
|
||||
const page = Math.floor(offset / limit) + 1;
|
||||
const summarySources = resultSummary?.sources?.join('、') || '—';
|
||||
@@ -424,8 +435,9 @@ export default function HistoryPage() {
|
||||
<ColumnVisibilityPanel visible={columnSettingsOpen} metrics={allMetrics} visibleKeys={visibleKeys} onToggle={toggleMetric} onShowAll={() => setVisibleMetrics(allMetrics.map((metric) => metric.key))} onReset={() => setVisibleMetrics(allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key))} onClose={() => setColumnSettingsOpen(false)} />
|
||||
<div
|
||||
className={`v2-history-table-scroll${mobileLayout ? ' is-mobile-scroll' : ''}`}
|
||||
tabIndex={mobileLayout ? 0 : undefined}
|
||||
aria-label={mobileLayout ? '历史明细列表,可上下滚动' : undefined}
|
||||
tabIndex={0}
|
||||
aria-label={mobileLayout ? '历史明细列表,可上下滚动' : '历史明细表格,可使用左右方向键浏览业务字段'}
|
||||
onKeyDown={scrollHistoryTable}
|
||||
>
|
||||
{mobileLayout
|
||||
? <div className="v2-history-mobile-list">{resultRows.map((row) => <Card key={row.id} className={`v2-history-mobile-card${selectedRow?.id === row.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" className="v2-history-mobile-action" aria-pressed={selectedRow?.id === row.id} aria-expanded={selectedRow?.id === row.id} aria-label={`查看 ${row.plate || row.vin} ${row.deviceTime} 数据详情`} onClick={() => selectRow(row)}><span className="v2-history-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><span className={`v2-quality is-${row.quality}`}><i />{historyQualityLabel(row.quality)}</span></header><p><PlatformTime className="v2-history-time" value={row.deviceTime} /><b>{row.protocol}</b></p><small className="v2-history-mobile-quality">{row.qualityReason || '平台未返回质量说明'}</small><dl>{visibleMetrics.slice(0, 4).map((metric) => <div key={metric.key}><dt>{metric.label}</dt><dd>{formatHistoryValue(row.values[metric.key], metric)}</dd></div>)}</dl><footer>查看完整详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
||||
@@ -445,7 +457,8 @@ export default function HistoryPage() {
|
||||
className="v2-history-detail-sidesheet"
|
||||
visible={mobileLayout && Boolean(selectedRow)}
|
||||
aria-label="历史数据详情"
|
||||
width="100%"
|
||||
placement="bottom"
|
||||
height="min(82dvh, 720px)"
|
||||
title={<div className="v2-history-mobile-sheet-title"><strong>数据详情</strong><span>{selectedRow ? `${selectedRow.plate || '未绑定车牌'} · ${selectedRow.protocol}` : '来源、质量与原始证据'}</span></div>}
|
||||
onCancel={closeSelectedRow}
|
||||
>
|
||||
@@ -455,7 +468,9 @@ export default function HistoryPage() {
|
||||
className="v2-history-export-sidesheet"
|
||||
visible={exportAllowed && exportJobsOpen}
|
||||
aria-label="历史数据导出任务"
|
||||
width={mobileLayout ? '100%' : 520}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 520}
|
||||
height={mobileLayout ? 'min(76dvh, 680px)' : undefined}
|
||||
title={<div className="v2-history-mobile-sheet-title"><strong>导出任务</strong><span>查看生成进度与下载记录</span></div>}
|
||||
onCancel={() => setExportJobsOpen(false)}
|
||||
>
|
||||
|
||||
@@ -6126,25 +6126,74 @@
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-body {
|
||||
scroll-padding-inline-start: 302px;
|
||||
scrollbar-color: #b7c4d4 #f4f7fa;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.v2-history-table-scroll:focus-visible {
|
||||
outline: 2px solid rgba(18, 104, 223, .38);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.v2-history-data-table :is(.semi-table-row-head, .semi-table-row-cell).semi-table-cell-fixed-left {
|
||||
z-index: 5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.v2-history-table-scroll .v2-history-data-table.semi-table-wrapper .semi-table-thead > .semi-table-row > .semi-table-row-head.semi-table-cell-fixed-left {
|
||||
z-index: 8;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell.semi-table-cell-fixed-left,
|
||||
.v2-history-data-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell.semi-table-cell-fixed-left {
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.v2-history-data-table :is(.semi-table-row-head, .semi-table-row-cell).v2-history-plate-column {
|
||||
box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72);
|
||||
}
|
||||
|
||||
.v2-history-table-scroll::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.v2-history-table-scroll::-webkit-scrollbar-track {
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-body::-webkit-scrollbar-track {
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.v2-history-table-scroll::-webkit-scrollbar-thumb {
|
||||
border: 2px solid #f4f7fa;
|
||||
border-radius: 999px;
|
||||
background: #b7c4d4;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-body::-webkit-scrollbar-thumb {
|
||||
border: 2px solid #f4f7fa;
|
||||
border-radius: 999px;
|
||||
background: #b7c4d4;
|
||||
}
|
||||
|
||||
.v2-history-table-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: #91a4ba;
|
||||
}
|
||||
|
||||
.v2-history-data-table .semi-table-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #91a4ba;
|
||||
}
|
||||
|
||||
@media (min-width: 681px) {
|
||||
.v2-history-page {
|
||||
height: 100%;
|
||||
@@ -6403,6 +6452,63 @@
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-history-column-sidesheet,
|
||||
.v2-history-detail-sidesheet,
|
||||
.v2-history-export-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9e3ef;
|
||||
border-bottom: 0;
|
||||
border-radius: 18px 18px 0 0;
|
||||
background: #f4f7fb;
|
||||
box-shadow: 0 -18px 52px rgba(20, 38, 66, .18);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-history-column-sidesheet,
|
||||
.v2-history-detail-sidesheet,
|
||||
.v2-history-export-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-header {
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid #dfe6ef;
|
||||
padding: 12px 15px 10px;
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-history-column-sidesheet,
|
||||
.v2-history-detail-sidesheet,
|
||||
.v2-history-export-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.v2-history-column-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.v2-history-column-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer {
|
||||
padding: 9px 12px 10px;
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-history-detail-sidesheet,
|
||||
.v2-history-export-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
padding: 8px 8px 12px;
|
||||
}
|
||||
|
||||
.v2-history-detail-sidesheet.semi-sidesheet-bottom .v2-history-evidence.semi-card,
|
||||
.v2-history-export-sidesheet.semi-sidesheet-bottom .v2-export-jobs.semi-card {
|
||||
border-radius: 11px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.v2-history-mobile-list {
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
|
||||
Reference in New Issue
Block a user