feat: optimize mobile access filters

This commit is contained in:
lingniu
2026-07-18 23:07:12 +08:00
parent a7297abdaf
commit c358faf17d
3 changed files with 187 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdC
import { accessIssueSummary, accessRowsToCSV, formatAccessTime, formatSeconds, updateProtocolThreshold } from '../domain/access';
import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
import { MetricActionButton } from '../shared/MetricActionButton';
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
import { TablePagination } from '../shared/TablePagination';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel';
@@ -349,12 +350,17 @@ export default function AccessPage() {
setOffset(0);
}, [mobileLayout]);
const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN);
const mobileFiltersOpen = mobileLayout && !filtersCollapsed;
useSideSheetA11y(Boolean(selected), '.v2-access-detail-sidesheet', 'v2-access-detail', '车辆接入详情', '关闭车辆接入详情');
useSideSheetA11y(editable && governanceOpen, '.v2-access-governance-sidesheet', 'v2-access-governance', '接入治理配置', '关闭接入治理配置');
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 apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); };
const submit = (event: FormEvent) => { event.preventDefault(); apply(draft); setFiltersCollapsed(true); };
const applyDraft = () => { apply(draft); setFiltersCollapsed(true); };
const resetFilters = () => { apply(EMPTY_FILTERS); setFiltersCollapsed(true); };
const submit = (event: FormEvent) => { event.preventDefault(); applyDraft(); };
const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data;
const activeFilterCount = Object.values(criteria).filter(Boolean).length;
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;
@@ -365,6 +371,12 @@ export default function AccessPage() {
event.preventDefault();
tableScroller.scrollBy({ left: event.key === 'ArrowRight' ? 260 : -260, behavior: 'smooth' });
};
const filterFields = <>
<label className="is-search"><span></span><Input aria-label="车辆" prefix={<IconSearch />} value={draft.keyword} onChange={(value) => setDraft({ ...draft, keyword: value })} placeholder="车牌 / VIN" /></label>
<label><span id="access-connection-label"></span><Select aria-labelledby="access-connection-label" value={draft.connectionState} onChange={(value) => setDraft({ ...draft, connectionState: String(value) })} optionList={[{ value: '', label: '全部状态' }, { value: 'attention', label: '需关注' }, { value: 'healthy', label: '已接来源正常' }, { value: 'master_data', label: '资料待维护' }, { value: 'degraded', label: '部分来源异常' }, { value: 'offline', label: '已接来源离线' }, { value: 'not_connected', label: '尚无来源' }]} /></label>
<label><span id="access-protocol-label"></span><Select aria-labelledby="access-protocol-label" value={draft.protocol} onChange={(value) => setDraft({ ...draft, protocol: String(value) })} optionList={[{ value: '', label: '全部协议' }, ...PROTOCOLS.map((item) => ({ value: item, label: item }))]} /></label>
<label><span id="access-oem-label"></span><Select aria-labelledby="access-oem-label" value={draft.oem} onChange={(value) => setDraft({ ...draft, oem: String(value) })} optionList={[{ value: '', label: '全部品牌' }, ...(summary?.oems.map((item) => ({ value: item.name, label: item.name })) ?? [])]} /></label>
</>;
return <div className="v2-access-page v2-access-page-v3">
<div className="v2-access-discovery-shell">
@@ -394,27 +406,48 @@ export default function AccessPage() {
</Dropdown>
</> : <>{editable ? <Button theme="light" icon={<IconSetting />} aria-haspopup="dialog" aria-controls="v2-access-governance" aria-expanded={governanceOpen} onClick={() => setGovernanceOpen(true)}>{unresolvedQuery.data?.total ? ` · ${unresolvedQuery.data.total}` : ''}</Button> : null}<Button theme="light" icon={<IconRefresh />} onClick={() => void refresh()}></Button></>}
/>
<WorkspaceFilterPanel
{mobileLayout ? <div className="v2-access-mobile-discovery">
<MobileFilterToggle
title="接入筛选"
summary={activeFilterCount ? `已启用 ${activeFilterCount}` : '全部主车辆'}
expanded={mobileFiltersOpen}
expandedLabel="关闭"
collapsedLabel="修改"
onToggle={() => setFiltersCollapsed((value) => !value)}
/>
<SideSheet
className="v2-access-mobile-filter-sidesheet"
visible={mobileFiltersOpen}
placement="bottom"
height="min(76dvh, 640px)"
aria-label="接入车辆筛选"
title={<div className="v2-access-sheet-title"><strong></strong><span></span></div>}
onCancel={() => setFiltersCollapsed(true)}
footer={<div className="v2-access-mobile-filter-footer"><Button theme="light" onClick={resetFilters}></Button><Button theme="solid" onClick={applyDraft}></Button></div>}
>
<form className="v2-access-mobile-filter-form" onSubmit={submit}>
<header><strong></strong><span>线</span></header>
<div>{filterFields}</div>
</form>
</SideSheet>
</div> : <WorkspaceFilterPanel
className="v2-access-filter-card-v3"
title="接入筛选"
description="车辆、接入状态、真实协议与品牌"
mobileSummary={Object.values(criteria).filter(Boolean).length ? `已启用 ${Object.values(criteria).filter(Boolean).length}` : '全部主车辆'}
mobileSummary={activeFilterCount ? `已启用 ${activeFilterCount}` : '全部主车辆'}
expanded={!filtersCollapsed}
status={Object.values(criteria).filter(Boolean).length ? `${Object.values(criteria).filter(Boolean).length} 项已启用` : '全部主车辆'}
statusColor={Object.values(criteria).filter(Boolean).length ? 'blue' : 'grey'}
status={activeFilterCount ? `${activeFilterCount} 项已启用` : '全部主车辆'}
statusColor={activeFilterCount ? 'blue' : 'grey'}
onToggle={() => setFiltersCollapsed((value) => !value)}
>
<form className="v2-access-filter-v3" onSubmit={submit}>
<label className="is-search"><span></span><Input aria-label="车辆" prefix={<IconSearch />} value={draft.keyword} onChange={(value) => setDraft({ ...draft, keyword: value })} placeholder="车牌 / VIN" /></label>
<label><span id="access-connection-label"></span><Select aria-labelledby="access-connection-label" value={draft.connectionState} onChange={(value) => setDraft({ ...draft, connectionState: String(value) })} optionList={[{ value: '', label: '全部状态' }, { value: 'attention', label: '需关注' }, { value: 'healthy', label: '已接来源正常' }, { value: 'master_data', label: '资料待维护' }, { value: 'degraded', label: '部分来源异常' }, { value: 'offline', label: '已接来源离线' }, { value: 'not_connected', label: '尚无来源' }]} /></label>
<label><span id="access-protocol-label"></span><Select aria-labelledby="access-protocol-label" value={draft.protocol} onChange={(value) => setDraft({ ...draft, protocol: String(value) })} optionList={[{ value: '', label: '全部协议' }, ...PROTOCOLS.map((item) => ({ value: item, label: item }))]} /></label>
<label><span id="access-oem-label"></span><Select aria-labelledby="access-oem-label" value={draft.oem} onChange={(value) => setDraft({ ...draft, oem: String(value) })} optionList={[{ value: '', label: '全部品牌' }, ...(summary?.oems.map((item) => ({ value: item.name, label: item.name })) ?? [])]} /></label>
{filterFields}
<div className="v2-access-filter-actions">
<Button className="v2-primary-button" theme="solid" htmlType="submit"></Button>
<Button className="v2-secondary-button" theme="light" htmlType="button" onClick={() => apply(EMPTY_FILTERS)}></Button>
<Button className="v2-secondary-button" theme="light" htmlType="button" onClick={resetFilters}></Button>
</div>
</form>
</WorkspaceFilterPanel>
</WorkspaceFilterPanel>}
</div>
{summaryQuery.isError ? <InlineError message={summaryQuery.error instanceof Error ? summaryQuery.error.message : '接入汇总读取失败'} onRetry={() => summaryQuery.refetch()} /> : null}
{vehiclesQuery.isError ? <InlineError message={vehiclesQuery.error instanceof Error ? vehiclesQuery.error.message : '接入车辆读取失败'} onRetry={() => vehiclesQuery.refetch()} /> : null}