60 lines
4.4 KiB
TypeScript
60 lines
4.4 KiB
TypeScript
import { IconClose, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
|
|
import { Button, Input, Tag } from '@douyinfe/semi-ui';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useDeferredValue, useMemo, useState } from 'react';
|
|
import { api } from '../../api/client';
|
|
import type { VehicleRow } from '../../api/types';
|
|
import { InlineError, PanelLoading } from './AsyncState';
|
|
|
|
const VEHICLE_SELECTION_LIMIT = 500;
|
|
|
|
function uniqueVehicles(rows: VehicleRow[]) {
|
|
const byVIN = new Map<string, VehicleRow>();
|
|
for (const row of rows) {
|
|
const current = byVIN.get(row.vin);
|
|
if (!current || (!current.online && row.online)) byVIN.set(row.vin, row);
|
|
}
|
|
return Array.from(byVIN.values());
|
|
}
|
|
|
|
export function VehicleQuickSelector({ value, protocol, onChange }: { value: string[]; protocol?: string; onChange: (value: string[]) => void }) {
|
|
const [search, setSearch] = useState('');
|
|
const deferredSearch = useDeferredValue(search.trim());
|
|
const params = useMemo(() => {
|
|
const next = new URLSearchParams({ limit: '12', offset: '0' });
|
|
if (deferredSearch) next.set('keyword', deferredSearch);
|
|
if (protocol) next.set('protocol', protocol);
|
|
return next;
|
|
}, [deferredSearch, protocol]);
|
|
const vehicles = useQuery({
|
|
queryKey: ['automation-vehicle-options', protocol ?? '', deferredSearch],
|
|
queryFn: ({ signal }) => api.vehicles(params, signal),
|
|
staleTime: 30_000
|
|
});
|
|
const options = useMemo(() => uniqueVehicles(vehicles.data?.items ?? []), [vehicles.data?.items]);
|
|
const selected = useMemo(() => new Set(value), [value]);
|
|
|
|
const toggle = (vin: string) => {
|
|
if (selected.has(vin)) {
|
|
onChange(value.filter((item) => item !== vin));
|
|
return;
|
|
}
|
|
onChange([...value, vin].slice(0, VEHICLE_SELECTION_LIMIT));
|
|
};
|
|
const selectOnline = () => {
|
|
const next = new Set(value);
|
|
for (const vehicle of options) if (vehicle.online) next.add(vehicle.vin);
|
|
onChange(Array.from(next).slice(0, VEHICLE_SELECTION_LIMIT));
|
|
};
|
|
|
|
return <section className="is-wide v2-vehicle-quick-selector" aria-label="车辆快捷选择">
|
|
<header><span><strong>快速选择车辆</strong><small>按车牌或 VIN 搜索;不选择表示覆盖当前协议下的全部授权车辆</small></span><Tag color={value.length ? 'blue' : 'grey'} type="light" size="small">{value.length ? `已选 ${value.length} 辆` : '全部车辆'}</Tag></header>
|
|
<div className="v2-vehicle-quick-search"><Input prefix={<IconSearch />} showClear value={search} onChange={setSearch} aria-label="搜索要应用自动化的车辆" placeholder="搜索车牌 / VIN / 终端号" /><Button htmlType="button" theme="light" type="tertiary" disabled={!options.some((item) => item.online)} onClick={selectOnline}>选择当前在线</Button>{value.length ? <Button htmlType="button" theme="borderless" type="danger" onClick={() => onChange([])}>清空</Button> : null}</div>
|
|
{value.length ? <div className="v2-vehicle-quick-selected" aria-label="已选择车辆">{value.map((vin) => <Tag key={vin} color="blue" type="light" closable onClose={() => toggle(vin)}>{vin}</Tag>)}</div> : null}
|
|
{vehicles.isPending ? <PanelLoading compact title="正在读取最近活跃车辆" description="可继续填写其他规则配置。" /> : vehicles.isError ? <InlineError message={vehicles.error instanceof Error ? vehicles.error.message : '车辆读取失败'} onRetry={() => vehicles.refetch()} /> : <div className="v2-vehicle-quick-results" role="listbox" aria-label={deferredSearch ? '车辆搜索结果' : '最近活跃车辆'}>
|
|
{options.map((vehicle) => <Button htmlType="button" key={vehicle.vin} className={selected.has(vehicle.vin) ? 'is-selected' : ''} theme="light" type={selected.has(vehicle.vin) ? 'primary' : 'tertiary'} role="option" aria-selected={selected.has(vehicle.vin)} onClick={() => toggle(vehicle.vin)}><span className="v2-vehicle-quick-status" aria-hidden="true">{selected.has(vehicle.vin) ? <IconTickCircle /> : <i className={vehicle.online ? 'is-online' : ''} />}</span><span><strong>{vehicle.plate || '未绑定车牌'}</strong><small>{vehicle.vin} · {vehicle.protocol}{vehicle.online ? ' · 在线' : ' · 离线'}</small></span>{selected.has(vehicle.vin) ? <IconClose className="v2-vehicle-quick-remove" aria-hidden="true" /> : null}</Button>)}
|
|
{!options.length ? <p>{deferredSearch ? '没有匹配车辆,请换一个车牌或 VIN。' : '当前协议下没有可快捷选择的车辆。'}</p> : null}
|
|
</div>}
|
|
</section>;
|
|
}
|