refine Semi UI access difference workspace
This commit is contained in:
@@ -66,6 +66,21 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
||||
expect(within(filterActions!).getByRole('button', { name: '重置' })).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-access-semi-table.semi-table-wrapper')).toBeInTheDocument();
|
||||
expect(view.container.querySelector('.v2-access-table-scroll-v3 > table')).not.toBeInTheDocument();
|
||||
const desktopAccessScroll = view.container.querySelector<HTMLElement>('.v2-access-table-scroll-v3');
|
||||
expect(desktopAccessScroll).toHaveAttribute('tabindex', '0');
|
||||
expect(desktopAccessScroll).toHaveAttribute('aria-label', '车辆接入表格,可使用左右方向键浏览协议来源');
|
||||
const desktopTableScroller = desktopAccessScroll?.querySelector<HTMLElement>('.semi-table-body');
|
||||
expect(desktopTableScroller).toBeInTheDocument();
|
||||
Object.defineProperty(desktopAccessScroll!, 'clientWidth', { configurable: true, value: 760 });
|
||||
Object.defineProperty(desktopAccessScroll!, 'scrollWidth', { configurable: true, value: 760 });
|
||||
const scrollBy = vi.fn();
|
||||
Object.defineProperty(desktopTableScroller!, 'scrollBy', { configurable: true, value: scrollBy });
|
||||
fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowRight' });
|
||||
expect(scrollBy).toHaveBeenCalledWith({ left: 260, behavior: 'smooth' });
|
||||
fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowLeft' });
|
||||
expect(scrollBy).toHaveBeenCalledWith({ left: -260, behavior: 'smooth' });
|
||||
expect(screen.getByRole('columnheader', { name: '车辆' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-vehicle-column');
|
||||
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-model-column');
|
||||
const desktopRow = screen.getByTestId('access-row-OLDVIN');
|
||||
expect(desktopRow).toHaveAttribute('role', 'button');
|
||||
expect(desktopRow).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -154,6 +169,7 @@ test('renders mobile access vehicles as selectable Semi cards', async () => {
|
||||
expect(within(action).getByText('32960')).toBeInTheDocument();
|
||||
expect(within(action).getByText('808')).toBeInTheDocument();
|
||||
expect(within(action).getByText('宇通')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('每页车辆数')).not.toBeInTheDocument();
|
||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(action);
|
||||
@@ -166,4 +182,19 @@ test('renders mobile access vehicles as selectable Semi cards', async () => {
|
||||
expect(dialog.querySelector('.v2-access-inspector-summary.semi-descriptions')).toBeInTheDocument();
|
||||
expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument();
|
||||
expect(dialog.querySelectorAll('.v2-access-protocol-detail.semi-card')).toHaveLength(3);
|
||||
expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
||||
expect(document.querySelector('.v2-access-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' });
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '关闭车辆接入详情' }));
|
||||
expect(action).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(action).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
|
||||
const governanceButton = screen.getByRole('button', { name: /接入治理/ });
|
||||
fireEvent.click(governanceButton);
|
||||
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 .semi-sidesheet-inner')).toHaveStyle({ height: 'min(86dvh, 760px)' });
|
||||
fireEvent.click(within(governanceDialog).getByRole('button', { name: '关闭接入治理配置' }));
|
||||
expect(governanceButton).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(document.body.style.overflow).not.toBe('hidden');
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IconChevronRight, IconClose, IconDownload, IconRefresh, IconSave, IconSearch, IconSetting } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Select, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { FormEvent, memo, useEffect, useMemo, useState } from 'react';
|
||||
import { FormEvent, KeyboardEvent, memo, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdConfig, AccessThresholdUpdate, AccessUnresolvedIdentity, AccessVehicleRow, Page } from '../../api/types';
|
||||
@@ -121,11 +121,11 @@ function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
|
||||
const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN, onSelect }: { rows: AccessVehicleRow[]; selectedVIN: string; onSelect: (vin: string) => void }) {
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '车辆', dataIndex: 'plate', width: 165,
|
||||
title: '车辆', dataIndex: 'plate', width: 165, fixed: 'left' as const, className: 'v2-access-vehicle-column',
|
||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.plate || '未绑定车牌'}</strong><span>{row.vin}</span></div>
|
||||
},
|
||||
{
|
||||
title: '品牌 / 车型', dataIndex: 'oem', width: 140,
|
||||
title: '品牌 / 车型', dataIndex: 'oem', width: 140, fixed: 'left' as const, className: 'v2-access-model-column',
|
||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
||||
},
|
||||
...PROTOCOLS.map((protocol) => ({
|
||||
@@ -140,6 +140,8 @@ const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN,
|
||||
|
||||
return <Table
|
||||
className="v2-access-semi-table"
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: 1005 }}
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey="vin"
|
||||
@@ -239,6 +241,15 @@ export default function AccessPage() {
|
||||
const submit = (event: FormEvent) => { event.preventDefault(); apply(draft); setFiltersCollapsed(true); };
|
||||
const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data;
|
||||
const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), ...(editable ? [unresolvedQuery.refetch(), thresholdQuery.refetch()] : [])]);
|
||||
const scrollAccessTable = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (mobileLayout || event.currentTarget !== event.target || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;
|
||||
const tableScroller = event.currentTarget.scrollWidth > event.currentTarget.clientWidth
|
||||
? event.currentTarget
|
||||
: event.currentTarget.querySelector<HTMLElement>('.semi-table-body');
|
||||
if (!tableScroller) return;
|
||||
event.preventDefault();
|
||||
tableScroller.scrollBy({ left: event.key === 'ArrowRight' ? 260 : -260, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return <div className="v2-access-page v2-access-page-v3">
|
||||
<div className="v2-access-discovery-shell">
|
||||
@@ -294,8 +305,9 @@ export default function AccessPage() {
|
||||
})}</nav>
|
||||
<div
|
||||
className={`v2-access-table-scroll-v3${mobileLayout ? ' is-mobile-scroll' : ''}`}
|
||||
tabIndex={mobileLayout ? 0 : undefined}
|
||||
aria-label={mobileLayout ? '车辆接入列表,可上下滚动' : undefined}
|
||||
tabIndex={0}
|
||||
aria-label={mobileLayout ? '车辆接入列表,可上下滚动' : '车辆接入表格,可使用左右方向键浏览协议来源'}
|
||||
onKeyDown={scrollAccessTable}
|
||||
>
|
||||
{mobileLayout
|
||||
? <div className="v2-access-mobile-list">{rows.map((row) => <Card key={row.vin} className={`v2-access-mobile-card${selected?.vin === row.vin ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" aria-pressed={selected?.vin === row.vin} aria-expanded={selected?.vin === row.vin} aria-label={`查看 ${row.plate || row.vin} 接入详情`} className="v2-access-mobile-action" onClick={() => setSelectedVIN(row.vin)}><span className="v2-access-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><ConnectionState row={row} /></header><p className={row.connectionState === 'healthy' ? '' : 'is-issue'} title={row.connectionState === 'healthy' ? undefined : accessIssueSummary(row)}>{row.connectionState === 'healthy' ? `${row.oem || '品牌未维护'} · ${row.model || row.company || '车型未维护'}` : `${accessIssueSummary(row)} · ${row.oem || '品牌未维护'} ${row.model || row.company || '车型未维护'}`}</p><span className="v2-access-mobile-protocols">{PROTOCOLS.map((protocol) => <ProtocolState key={protocol} protocolLabel={compactProtocolLabel(protocol)} status={statusByProtocol(row, protocol)} />)}</span><footer>查看接入详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
||||
@@ -303,14 +315,16 @@ export default function AccessPage() {
|
||||
{vehiclesQuery.isFetching ? <PanelLoading className="v2-access-loading" title="正在更新车辆接入状态…" description="当前列表返回后会自动替换。" compact={Boolean(rows.length)} /> : null}
|
||||
{!vehiclesQuery.isFetching && !rows.length ? <PanelEmpty className="v2-access-empty" title="没有匹配车辆" description="调整车牌、协议或接入状态筛选后重试。" /> : null}
|
||||
</div>
|
||||
<footer><TablePagination page={page} totalPages={totalPages} info={`共 ${(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆`} onPageChange={(next) => setOffset((next - 1) * limit)} pageSize={limit} pageSizeLabel="每页车辆数" onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={[{ value: 20, label: '20 辆/页' }, { value: 50, label: '50 辆/页' }, { value: 100, label: '100 辆/页' }]} /></footer>
|
||||
<footer><TablePagination page={page} totalPages={totalPages} info={`共 ${(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆`} onPageChange={(next) => setOffset((next - 1) * limit)} pageSize={mobileLayout ? undefined : limit} pageSizeLabel="每页车辆数" onPageSizeChange={(next) => { setLimit(next); setOffset(0); }} pageSizeOptions={mobileLayout ? undefined : [{ value: 20, label: '20 辆/页' }, { value: 50, label: '50 辆/页' }, { value: 100, label: '100 辆/页' }]} /></footer>
|
||||
</Card>
|
||||
</div>
|
||||
<SideSheet
|
||||
className="v2-access-detail-sidesheet"
|
||||
visible={Boolean(selected)}
|
||||
aria-label="车辆接入详情"
|
||||
width={mobileLayout ? '100%' : 520}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 520}
|
||||
height={mobileLayout ? 'min(84dvh, 740px)' : undefined}
|
||||
title={<div className="v2-access-sheet-title"><strong>车辆接入详情</strong><span>{selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '来源、在线状态与接入证据'}</span></div>}
|
||||
onCancel={() => setSelectedVIN('')}
|
||||
>
|
||||
@@ -320,7 +334,9 @@ export default function AccessPage() {
|
||||
className="v2-access-governance-sidesheet"
|
||||
visible={governanceOpen}
|
||||
aria-label="接入治理配置"
|
||||
width={560}
|
||||
placement={mobileLayout ? 'bottom' : 'right'}
|
||||
width={mobileLayout ? undefined : 560}
|
||||
height={mobileLayout ? 'min(86dvh, 760px)' : undefined}
|
||||
title={<div className="v2-access-sheet-title"><strong>接入治理</strong><span>集中处理待绑定来源与在线判定阈值</span></div>}
|
||||
onCancel={() => setGovernanceOpen(false)}
|
||||
footer={<Button theme="solid" onClick={() => setGovernanceOpen(false)}>完成</Button>}
|
||||
|
||||
@@ -8632,6 +8632,67 @@
|
||||
min-width: 1005px;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 {
|
||||
scrollbar-color: #b7c4d4 #f4f7fa;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3:not(.is-mobile-scroll) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 > .v2-access-semi-table.semi-table-wrapper {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3:focus-visible {
|
||||
outline: 2px solid rgba(18, 104, 243, .34);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-body {
|
||||
overflow-x: auto;
|
||||
scroll-padding-inline-start: 305px;
|
||||
scrollbar-color: #b7c4d4 #f4f7fa;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.v2-access-semi-table :is(.semi-table-row-head, .semi-table-row-cell).semi-table-cell-fixed-left {
|
||||
z-index: 5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 .v2-access-semi-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-access-semi-table .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell.semi-table-cell-fixed-left,
|
||||
.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell.semi-table-cell-fixed-left {
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.v2-access-semi-table :is(.semi-table-row-head, .semi-table-row-cell).v2-access-model-column {
|
||||
box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72);
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-body::-webkit-scrollbar-track {
|
||||
background: #f4f7fa;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-body::-webkit-scrollbar-thumb {
|
||||
border: 2px solid #f4f7fa;
|
||||
border-radius: 999px;
|
||||
background: #b7c4d4;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 th:nth-child(1) { width: 165px; }
|
||||
.v2-access-table-scroll-v3 th:nth-child(2) { width: 140px; }
|
||||
.v2-access-table-scroll-v3 th:nth-child(3),
|
||||
@@ -9026,11 +9087,90 @@
|
||||
}
|
||||
|
||||
.v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-button.semi-button,
|
||||
.v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-current,
|
||||
.v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-size.semi-select {
|
||||
.v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-current {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-access-detail-sidesheet,
|
||||
.v2-access-governance-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-access-detail-sidesheet,
|
||||
.v2-access-governance-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-header {
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid #dfe6ef;
|
||||
padding: 12px 15px 10px;
|
||||
}
|
||||
|
||||
:is(
|
||||
.v2-access-detail-sidesheet,
|
||||
.v2-access-governance-sidesheet
|
||||
).semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.v2-access-detail-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
padding: 0 7px 12px;
|
||||
}
|
||||
|
||||
.v2-access-detail-sidesheet.semi-sidesheet-bottom .v2-access-inspector-v3.is-sheet.semi-card {
|
||||
border-radius: 11px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet.semi-sidesheet-bottom .semi-sidesheet-body {
|
||||
padding: 8px 8px 12px;
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer {
|
||||
padding: 9px 12px 10px;
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet.semi-sidesheet-bottom .semi-sidesheet-footer .semi-button {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet .v2-access-governance-summary > .semi-card-body {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet .v2-access-governance-summary > .semi-card-body > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet .v2-access-governance-summary > .semi-card-body > div + div {
|
||||
border-top: 0;
|
||||
border-left: 1px solid var(--v2-border);
|
||||
}
|
||||
|
||||
.v2-access-governance-sidesheet .v2-access-governance-summary strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.v2-access-detail-sidesheet .v2-access-protocol-details.semi-card-group {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user