refine Semi UI monitor workspace
This commit is contained in:
@@ -2,7 +2,7 @@ import {
|
||||
IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin,
|
||||
IconQrCode, IconRefresh, IconSearch
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Button, ButtonGroup, Card, Empty, Input, Modal, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, Empty, Input, Modal, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
@@ -10,6 +10,7 @@ import { api } from '../../api/client';
|
||||
import type { Page, VehicleRealtimeRow } from '../../api/types';
|
||||
import { FleetMap } from '../map/FleetMap';
|
||||
import { EmptyState, InlineError } from '../shared/AsyncState';
|
||||
import { SegmentedTabs } from '../shared/SegmentedTabs';
|
||||
import { TablePagination } from '../shared/TablePagination';
|
||||
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
|
||||
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
|
||||
@@ -23,6 +24,10 @@ const protocols = ['', 'GB32960', 'JT808', 'YUTONG_MQTT'];
|
||||
const statuses = ['', 'online', 'offline', 'driving', 'idle', 'no_location'];
|
||||
const EMPTY_VEHICLES: VehicleRealtimeRow[] = [];
|
||||
const MOBILE_MONITOR_QUERY = '(max-width: 760px)';
|
||||
const MONITOR_VIEW_ITEMS = [
|
||||
{ key: 'map', label: '地图', icon: <IconMapPin aria-hidden="true" /> },
|
||||
{ key: 'list', label: '列表', icon: <IconList aria-hidden="true" /> }
|
||||
] as const;
|
||||
|
||||
function BatchVehicleSearchDialog({ initialValue, onApply, onClose }: { initialValue: string; onApply: (value: string) => void; onClose: () => void }) {
|
||||
const [draft, setDraft] = useState(initialValue);
|
||||
@@ -407,6 +412,17 @@ export default function MonitorPage() {
|
||||
const selectMapVehicle = useCallback((vehicle: VehicleRealtimeRow) => selectVehicle(vehicle.vin), [selectVehicle]);
|
||||
const collapseDetail = useCallback(() => setDetailOpen(false), []);
|
||||
const expandDetail = useCallback(() => setDetailOpen(true), []);
|
||||
const changeMonitorMode = useCallback((nextMode: 'map' | 'list') => {
|
||||
setMode(nextMode);
|
||||
if (nextMode === 'list') setDetailOpen(false);
|
||||
}, []);
|
||||
const clearFilters = useCallback(() => {
|
||||
setKeyword('');
|
||||
setProtocol('');
|
||||
setStatus('');
|
||||
setListOffset(0);
|
||||
}, []);
|
||||
const activeFilterCount = Number(Boolean(keyword.trim())) + Number(Boolean(protocol)) + Number(Boolean(status));
|
||||
const driving = rows.filter((vehicle) => vehicleStatus(vehicle) === 'driving').length;
|
||||
const idle = rows.filter((vehicle) => vehicleStatus(vehicle) === 'idle').length;
|
||||
const offline = rows.filter((vehicle) => vehicleStatus(vehicle) === 'offline').length;
|
||||
@@ -414,35 +430,52 @@ export default function MonitorPage() {
|
||||
return (
|
||||
<div className="v2-monitor-page">
|
||||
<Card className="v2-filterbar" bodyStyle={{ padding: 0 }} aria-label="车辆筛选">
|
||||
<div className={`v2-search-field${searchTerms.length > 1 ? ' is-batch' : ''}`}>
|
||||
<Input
|
||||
aria-label="搜索车辆"
|
||||
prefix={<IconSearch />}
|
||||
value={keyword}
|
||||
onChange={(value) => { setKeyword(value); setListOffset(0); }}
|
||||
onPaste={(event) => {
|
||||
const pastedTerms = parseMonitorSearchTerms(event.clipboardData.getData('text'));
|
||||
if (pastedTerms.length <= 1) return;
|
||||
event.preventDefault();
|
||||
setKeyword(pastedTerms.join(','));
|
||||
setListOffset(0);
|
||||
}}
|
||||
placeholder="车牌 / VIN;可批量粘贴车牌"
|
||||
suffix={<Button className="v2-search-batch-action" size="small" theme="borderless" onClick={() => setBatchSearchOpen(true)}>批量</Button>}
|
||||
/>
|
||||
{searchTerms.length > 1 ? <span className={`v2-search-batch-count${batchMatch.missing.length && !batchSearchPending ? ' has-missing' : ''}`} aria-live="polite" title={searchTerms.length === MAX_MONITOR_SEARCH_TERMS ? `最多支持 ${MAX_MONITOR_SEARCH_TERMS} 条;${batchStatusTitle}` : batchStatusTitle}>{batchSearchPending ? `已识别 ${searchTerms.length} 辆` : `已找到 ${batchMatch.matched}/${searchTerms.length}`}</span> : null}
|
||||
<div className="v2-monitor-filter-fields" aria-label="筛选条件">
|
||||
<div className={`v2-search-field${searchTerms.length > 1 ? ' is-batch' : ''}`}>
|
||||
<Input
|
||||
aria-label="搜索车辆"
|
||||
prefix={<IconSearch />}
|
||||
value={keyword}
|
||||
onChange={(value) => { setKeyword(value); setListOffset(0); }}
|
||||
onPaste={(event) => {
|
||||
const pastedTerms = parseMonitorSearchTerms(event.clipboardData.getData('text'));
|
||||
if (pastedTerms.length <= 1) return;
|
||||
event.preventDefault();
|
||||
setKeyword(pastedTerms.join(','));
|
||||
setListOffset(0);
|
||||
}}
|
||||
placeholder="车牌 / VIN;可批量粘贴车牌"
|
||||
suffix={<Button className="v2-search-batch-action" size="small" theme="borderless" onClick={() => setBatchSearchOpen(true)}>批量</Button>}
|
||||
/>
|
||||
{searchTerms.length > 1 ? <span className={`v2-search-batch-count${batchMatch.missing.length && !batchSearchPending ? ' has-missing' : ''}`} aria-live="polite" title={searchTerms.length === MAX_MONITOR_SEARCH_TERMS ? `最多支持 ${MAX_MONITOR_SEARCH_TERMS} 条;${batchStatusTitle}` : batchStatusTitle}>{batchSearchPending ? `已识别 ${searchTerms.length} 辆` : `已找到 ${batchMatch.matched}/${searchTerms.length}`}</span> : null}
|
||||
</div>
|
||||
<span className="v2-sr-only" id="monitor-protocol-filter-label">协议</span>
|
||||
<Select value={protocol} onChange={(value) => { setProtocol(String(value)); setListOffset(0); }} aria-labelledby="monitor-protocol-filter-label" optionList={protocols.map((item) => ({ value: item, label: item || '全部协议' }))} />
|
||||
<span className="v2-sr-only" id="monitor-status-filter-label">在线状态</span>
|
||||
<Select value={status} onChange={(value) => { setStatus(String(value)); setListOffset(0); }} aria-labelledby="monitor-status-filter-label" optionList={statuses.map((item) => ({ value: item, label: item === 'no_location' ? '无实时位置' : item ? statusLabel(item as never) : '全部状态' }))} />
|
||||
<Button className="v2-filter-reset" icon={<IconRefresh />} disabled={activeFilterCount === 0} onClick={clearFilters}>清空</Button>
|
||||
</div>
|
||||
<div className="v2-monitor-filter-actions" aria-label="监控操作">
|
||||
<span
|
||||
className={`v2-filter-live-status${activeFilterCount ? ' has-filters' : ''}`}
|
||||
role="status"
|
||||
aria-label={`搜索和筛选条件修改后自动生效,已启用 ${activeFilterCount} 个条件`}
|
||||
title="搜索和筛选条件修改后自动生效"
|
||||
>
|
||||
<IconFilter aria-hidden="true" />
|
||||
<span className="v2-filter-live-status-label">自动生效</span>
|
||||
{activeFilterCount ? <b>{activeFilterCount}</b> : null}
|
||||
</span>
|
||||
<SegmentedTabs
|
||||
ariaLabel="监控视图"
|
||||
className="v2-monitor-mode"
|
||||
value={mode}
|
||||
items={MONITOR_VIEW_ITEMS}
|
||||
onChange={changeMonitorMode}
|
||||
variant="filled"
|
||||
/>
|
||||
<Button className="v2-monitor-mobile-entry" theme="light" icon={<IconQrCode />} aria-label="打开手机端入口" onClick={() => setMobileEntryOpen(true)}><span className="v2-monitor-mobile-entry-label">手机端</span></Button>
|
||||
</div>
|
||||
<span className="v2-sr-only" id="monitor-protocol-filter-label">协议</span>
|
||||
<Select value={protocol} onChange={(value) => { setProtocol(String(value)); setListOffset(0); }} aria-labelledby="monitor-protocol-filter-label" optionList={protocols.map((item) => ({ value: item, label: item || '全部协议' }))} />
|
||||
<span className="v2-sr-only" id="monitor-status-filter-label">在线状态</span>
|
||||
<Select value={status} onChange={(value) => { setStatus(String(value)); setListOffset(0); }} aria-labelledby="monitor-status-filter-label" optionList={statuses.map((item) => ({ value: item, label: item === 'no_location' ? '无实时位置' : item ? statusLabel(item as never) : '全部状态' }))} />
|
||||
<Button className="v2-filter-reset" icon={<IconRefresh />} onClick={() => { setKeyword(''); setProtocol(''); setStatus(''); setListOffset(0); }}>清空</Button>
|
||||
<span className="v2-filter-live-status" role="status" title="搜索和筛选条件修改后自动生效"><IconFilter />实时筛选</span>
|
||||
<ButtonGroup className="v2-monitor-mode" aria-label="监控视图">
|
||||
<Button aria-pressed={mode === 'map'} className={mode === 'map' ? 'is-active' : ''} theme={mode === 'map' ? 'solid' : 'borderless'} type={mode === 'map' ? 'primary' : 'tertiary'} icon={<IconMapPin />} onClick={() => setMode('map')}>地图</Button>
|
||||
<Button aria-pressed={mode === 'list'} className={mode === 'list' ? 'is-active' : ''} theme={mode === 'list' ? 'solid' : 'borderless'} type={mode === 'list' ? 'primary' : 'tertiary'} icon={<IconList />} onClick={() => { setMode('list'); setDetailOpen(false); }}>列表</Button>
|
||||
</ButtonGroup>
|
||||
<Button className="v2-monitor-mobile-entry" theme="light" icon={<IconQrCode />} onClick={() => setMobileEntryOpen(true)}>手机端</Button>
|
||||
</Card>
|
||||
{batchSearchOpen ? <BatchVehicleSearchDialog initialValue={searchTerms.join('\n')} onClose={() => setBatchSearchOpen(false)} onApply={(value) => { setKeyword(value); setListOffset(0); setBatchSearchOpen(false); }} /> : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user