diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx
index a853dc2c..b4b4a599 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx
@@ -179,6 +179,26 @@ test('pastes, deduplicates, and submits multiple plates as one batch search', as
});
});
+test('opens an explicit batch editor and applies copied plate rows', async () => {
+ const vehicleRealtime = vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: vehicles, total: 2, limit: 50, offset: 0 });
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: '批量' }));
+ const editor = screen.getByRole('textbox', { name: /每行一个车牌/ });
+ fireEvent.change(editor, { target: { value: '粤a12345\n粤B67890\n粤A12345' } });
+ expect(screen.getByText('2', { selector: '.v2-batch-search-summary strong' })).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: '应用搜索(2)' }));
+
+ expect(screen.queryByRole('dialog', { name: '批量搜索车辆' })).not.toBeInTheDocument();
+ expect(screen.getByRole('textbox', { name: '搜索车辆' })).toHaveValue('粤A12345,粤B67890');
+ fireEvent.click(screen.getByRole('button', { name: /列表/ }));
+ await waitFor(() => {
+ const params = vehicleRealtime.mock.calls[vehicleRealtime.mock.calls.length - 1]?.[0];
+ expect(params?.get('keywords')).toBe('粤A12345,粤B67890');
+ });
+});
+
test('hides stale fleet rows while a pasted batch is still loading', () => {
monitorQueryFlags.isPlaceholderData = true;
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx
index 6e92e9c2..18821d2c 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx
@@ -16,6 +16,26 @@ const statuses = ['', 'online', 'offline', 'driving', 'idle'];
const EMPTY_VEHICLES: VehicleRealtimeRow[] = [];
const MOBILE_MONITOR_QUERY = '(max-width: 760px)';
+function BatchVehicleSearchDialog({ initialValue, onApply, onClose }: { initialValue: string; onApply: (value: string) => void; onClose: () => void }) {
+ const [draft, setDraft] = useState(initialValue);
+ const terms = useMemo(() => parseMonitorSearchTerms(draft), [draft]);
+ useEffect(() => {
+ const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); };
+ window.addEventListener('keydown', closeOnEscape);
+ return () => window.removeEventListener('keydown', closeOnEscape);
+ }, [onClose]);
+
+ return
{ if (event.target === event.currentTarget) onClose(); }}>
+
+
+
+
+
;
+}
+
function useMobileMonitorLayout() {
const [mobile, setMobile] = useState(() => window.matchMedia(MOBILE_MONITOR_QUERY).matches);
useEffect(() => {
@@ -181,6 +201,7 @@ export default function MonitorPage() {
const [listOffset, setListOffset] = useState(0);
const [listLimit, setListLimit] = useState(50);
const [keyword, setKeyword] = useState('');
+ const [batchSearchOpen, setBatchSearchOpen] = useState(false);
const deferredKeyword = useDeferredValue(keyword);
const searchTerms = useMemo(() => parseMonitorSearchTerms(keyword), [keyword]);
const [protocol, setProtocol] = useState('');
@@ -255,7 +276,7 @@ export default function MonitorPage() {
return (
+ {batchSearchOpen ?
setBatchSearchOpen(false)} onApply={(value) => { setKeyword(value); setListOffset(0); setBatchSearchOpen(false); }} /> : null}
{[
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
index d16b5263..fde7f63f 100644
--- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css
+++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css
@@ -62,6 +62,24 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-search-field.is-batch { border-color: #9fc0f7; background: #fbfdff; }
.v2-search-batch-count { flex: 0 0 auto; border-radius: 999px; background: var(--v2-blue-soft); padding: 3px 7px; color: var(--v2-blue); font-size: 10px; font-weight: 700; line-height: 1; white-space: nowrap; }
.v2-search-batch-count.has-missing { background: #fff4e5; color: #b45309; }
+.v2-search-batch-action { flex: 0 0 auto; border: 0; border-left: 1px solid #dce4ef; background: transparent; padding: 0 2px 0 10px; color: var(--v2-blue); font-size: 11px; font-weight: 700; white-space: nowrap; cursor: pointer; }
+.v2-search-batch-action:hover { color: #124ea1; }
+.v2-batch-search-backdrop { position: fixed; z-index: 1300; inset: 0; display: grid; place-items: center; background: rgba(15, 27, 46, .4); padding: 20px; backdrop-filter: blur(3px); }
+.v2-batch-search-dialog { width: min(520px, 100%); border: 1px solid rgba(255,255,255,.75); border-radius: 14px; background: #fff; padding: 20px; box-shadow: 0 24px 70px rgba(13, 33, 62, .24); }
+.v2-batch-search-dialog > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
+.v2-batch-search-dialog > header div { display: grid; gap: 4px; }
+.v2-batch-search-dialog > header strong { color: #17253a; font-size: 17px; }
+.v2-batch-search-dialog > header span { color: #728096; font-size: 12px; }
+.v2-batch-search-dialog > header button { display: grid; width: 30px; height: 30px; flex: 0 0 auto; place-items: center; border: 0; border-radius: 7px; background: #f3f6fa; color: #627086; cursor: pointer; }
+.v2-batch-search-dialog > label { display: block; margin-bottom: 8px; color: #536176; font-size: 12px; font-weight: 600; }
+.v2-batch-search-dialog textarea { display: block; width: 100%; min-height: 210px; resize: vertical; border: 1px solid #ccd8e7; border-radius: 9px; background: #fbfcfe; padding: 12px 14px; color: #1d2a3d; outline: 0; font: 13px/1.75 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
+.v2-batch-search-dialog textarea:focus { border-color: #4386df; background: #fff; box-shadow: 0 0 0 3px rgba(45, 116, 210, .1); }
+.v2-batch-search-summary { display: flex; justify-content: space-between; gap: 12px; margin-top: 9px; color: #758297; font-size: 11px; }
+.v2-batch-search-summary strong { color: var(--v2-blue); font-size: 13px; }
+.v2-batch-search-summary em { color: #8994a5; font-style: normal; }
+.v2-batch-search-dialog > footer { display: flex; justify-content: flex-end; gap: 9px; margin-top: 18px; }
+.v2-batch-search-dialog > footer button { min-width: 92px; }
+.v2-batch-search-dialog > footer button:disabled { opacity: .45; cursor: not-allowed; }
.v2-filterbar select { height: 36px; border: 1px solid #dce4ef; border-radius: 7px; background: #fff; padding: 0 30px 0 11px; color: #4d5c70; outline: 0; font-size: 12px; }
.v2-primary-button, .v2-secondary-button { display: flex; height: 36px; align-items: center; justify-content: center; gap: 7px; border-radius: 7px; padding: 0 14px; cursor: pointer; font-size: 12px; font-weight: 700; }
.v2-primary-button { border: 1px solid var(--v2-blue); background: var(--v2-blue); color: #fff; box-shadow: 0 5px 12px rgba(18,104,243,.18); }
@@ -1302,6 +1320,10 @@ button, a { -webkit-tap-highlight-color: transparent; }
}
@media (max-width: 760px) {
+ .v2-batch-search-backdrop { align-items: end; padding: 0; }
+ .v2-batch-search-dialog { width: 100%; border-radius: 16px 16px 0 0; padding: 18px 16px calc(16px + env(safe-area-inset-bottom)); }
+ .v2-batch-search-dialog textarea { min-height: 32vh; font-size: 16px; }
+ .v2-batch-search-dialog > footer button { min-width: 0; flex: 1; height: 44px; }
.v2-access-page-v3 { gap: 9px; padding: 10px 8px 18px; }
.v2-access-heading { align-items: flex-start; flex-direction: column; gap: 8px; }
.v2-access-heading h2 { font-size: 19px; }