refine Semi UI access difference workspace

This commit is contained in:
lingniu
2026-07-18 13:14:39 +08:00
parent ad81d108a7
commit c7123f6fbd
3 changed files with 197 additions and 10 deletions

View File

@@ -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>}