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 f06d8946..87cec160 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
@@ -396,10 +396,23 @@ test('mounts only the mobile list representation and removes its viewport listen
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = render();
+ const filterFields = view.container.querySelector('#monitor-filter-fields');
+ expect(filterFields).toHaveClass('is-mobile-collapsed');
+ const expandFilters = screen.getByRole('button', { name: '展开车辆筛选,当前 0 个条件' });
+ expect(expandFilters).toHaveAttribute('aria-expanded', 'false');
+ fireEvent.click(expandFilters);
+ expect(filterFields).not.toHaveClass('is-mobile-collapsed');
+ const collapseFilters = screen.getByRole('button', { name: '收起车辆筛选,当前 0 个条件' });
+ expect(collapseFilters).toHaveAttribute('aria-expanded', 'true');
+ fireEvent.click(collapseFilters);
+ expect(filterFields).toHaveClass('is-mobile-collapsed');
+
fireEvent.click(screen.getByRole('tab', { name: /列表/ }));
expect(await screen.findByText('车辆实时列表')).toBeInTheDocument();
await waitFor(() => expect(view.container.querySelectorAll('.v2-monitor-mobile-cards .v2-monitor-mobile-card.semi-card')).toHaveLength(1));
expect(view.container.querySelector('.v2-monitor-table-scroll')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: '在地图中定位粤A12345' })).toHaveTextContent('地图');
+ expect(view.container.querySelector('.v2-monitor-mobile-card footer')).not.toBeInTheDocument();
expect(addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
view.unmount();
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 797a2c36..177d3513 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,7 @@ import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
import { formatNumber, relativeFreshness, statusLabel, vehicleStatus } from '../domain/monitor';
import { formatTelemetryValue } from '../domain/telemetry';
+import { useMobileLayout } from '../hooks/useMobileLayout';
import { MAX_MONITOR_SEARCH_TERMS, MONITOR_REFRESH, monitorFilterScope, monitorQueryParams, parseMonitorSearchTerms, useMonitorData, useMonitorVehicleCard, type MonitorViewport } from '../hooks/useMonitorData';
import { LIVE_QUERY_POLICY, QUERY_MEMORY, retainPreviousPageWithinScope } from '../queryPolicy';
import { buildMonitorPath, parseMonitorRouteContext, withMonitorReturn } from '../routing/monitorContext';
@@ -23,7 +24,6 @@ import { buildMonitorPath, parseMonitorRouteContext, withMonitorReturn } from '.
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: },
{ key: 'list', label: '列表', icon: }
@@ -52,18 +52,6 @@ function BatchVehicleSearchDialog({ initialValue, onApply, onClose }: { initialV
;
}
-function useMobileMonitorLayout() {
- const [mobile, setMobile] = useState(() => window.matchMedia(MOBILE_MONITOR_QUERY).matches);
- useEffect(() => {
- const media = window.matchMedia(MOBILE_MONITOR_QUERY);
- const update = () => setMobile(media.matches);
- media.addEventListener('change', update);
- update();
- return () => media.removeEventListener('change', update);
- }, []);
- return mobile;
-}
-
type AddressCoordinate = { longitude: number; latitude: number; key: string };
function hasRealtimeLocation(vehicle: VehicleRealtimeRow) {
@@ -122,8 +110,7 @@ const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehic
return
{addressQuery.data?.formattedAddress || '暂无地址'}{addressQuery.dataUpdatedAt ? `解析于 ${new Date(addressQuery.dataUpdatedAt).toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })}` : ''}{moved ? : null}
;
});
-function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) {
- const mobile = useMobileMonitorLayout();
+function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, mobile, onSelect, onPage, onLimit }: { rows: VehicleRealtimeRow[]; total: number; page: number; totalPages: number; limit: number; loading: boolean; mobile: boolean; onSelect: (vin: string) => void; onPage: (page: number) => void; onLimit: (limit: number) => void }) {
const columns = useMemo(() => [
{ title: '车辆', dataIndex: 'plate', width: 210, className: 'v2-monitor-table-vehicle', render: (_value: string, row: VehicleRealtimeRow) => },
{ title: '速度', dataIndex: 'speedKmh', width: 120, render: (_value: number, row: VehicleRealtimeRow) => <>{hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null}> },
@@ -141,9 +128,8 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, on
/>
{!mobile ? {loading ?
: null}{!loading && !rows.length ?
: null}
: null}
{mobile ? {rows.map((row) =>
-
+ {row.plate || '未绑定车牌'}{row.vin}
{hasRealtimeSpeed(row) ? formatNumber(row.speedKmh, 1) : '—'}{hasRealtimeSpeed(row) ? km/h : null}} aria-label={`在地图中定位${row.plate || row.vin}`} onClick={() => onSelect(row.vin)}>地图
- 当日里程
- {hasTodayMileage(row) ? `${formatNumber(row.todayMileageKm, 1)} km` : '—'}
- 总里程
- {hasRealtimeMileage(row) ? `${formatNumber(row.totalMileageKm, 1)} km` : '—'}
- 协议来源
- {row.primaryProtocol || '未知'}{row.protocols.length > 1 ? ` · ${row.protocols.length} 路` : ''}
- 经纬度
- {hasRealtimeLocation(row) ?
{row.longitude.toFixed(6)}, {row.latitude.toFixed(6)} : '—'} - 地理位置
-
)}{loading ?
: null}{!loading && !rows.length ?
: null}
: null}
;
@@ -329,12 +315,14 @@ export default function MonitorPage() {
const [routeParams, setRouteParams] = useSearchParams();
const initialContextRef = useRef(parseMonitorRouteContext(routeParams));
const initialContext = initialContextRef.current;
+ const mobileLayout = useMobileLayout(760);
const [mode, setMode] = useState<'map' | 'list'>(initialContext.mode);
const [mobileEntryOpen, setMobileEntryOpen] = useState(false);
const [listOffset, setListOffset] = useState(initialContext.listOffset);
const [listLimit, setListLimit] = useState(initialContext.listLimit);
const [keyword, setKeyword] = useState(initialContext.keyword);
const [batchSearchOpen, setBatchSearchOpen] = useState(false);
+ const [filtersCollapsed, setFiltersCollapsed] = useState(true);
const deferredKeyword = useDeferredValue(keyword);
const searchTerms = useMemo(() => parseMonitorSearchTerms(keyword), [keyword]);
const [protocol, setProtocol] = useState(initialContext.protocol);
@@ -437,7 +425,7 @@ export default function MonitorPage() {
return (
-
+
1 ? ' is-batch' : ''}`}>
} disabled={activeFilterCount === 0} onClick={clearFilters}>清空
-
}
+ aria-controls="monitor-filter-fields"
+ aria-expanded={!filtersCollapsed}
+ aria-label={`${filtersCollapsed ? '展开' : '收起'}车辆筛选,当前 ${activeFilterCount} 个条件`}
+ onClick={() => setFiltersCollapsed((value) => !value)}
+ >
+ {filtersCollapsed ? '筛选' : '收起'}{activeFilterCount ?
{activeFilterCount} : null}
+ :
自动生效
{activeFilterCount ?
{activeFilterCount} : null}
-
+ }
) : null}
- : setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
+ : setListOffset((page - 1) * listLimit)} onLimit={(next) => { setListLimit(next); setListOffset(0); }} />}
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
index 0a21758c..1f1f9385 100644
--- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
+++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
@@ -11409,6 +11409,10 @@
}
@media (max-width: 760px) {
+ .v2-monitor-filter-fields.is-mobile-collapsed {
+ display: none;
+ }
+
.v2-filterbar > .semi-card-body {
display: grid;
gap: 8px;
@@ -11439,24 +11443,106 @@
display: grid;
width: 100%;
min-width: 0;
- grid-template-columns: auto minmax(0, 1fr) auto;
+ grid-template-columns: 70px minmax(0, 1fr) 42px;
gap: 7px;
}
- .v2-filter-live-status {
- width: 40px;
- height: 40px;
- padding: 0;
+ .v2-monitor-filter-toggle.semi-button {
+ width: 70px;
+ height: 42px;
+ justify-content: center;
+ gap: 5px;
+ border-color: #d7e2ef;
+ border-radius: 9px;
+ background: #f6f9fd;
+ color: #52677f;
+ padding: 0 8px;
+ font-size: 11px;
+ font-weight: 700;
}
- .v2-filter-live-status-label {
- display: none;
+ .v2-monitor-filter-toggle.semi-button[aria-expanded="true"],
+ .v2-monitor-filter-toggle.semi-button.has-filters {
+ border-color: #b7cff0;
+ background: #edf5ff;
+ color: #2465b7;
}
- .v2-filter-live-status b {
- position: absolute;
- top: -3px;
- right: -3px;
+ .v2-monitor-filter-toggle.semi-button b {
+ display: inline-grid;
+ min-width: 18px;
+ height: 18px;
+ place-items: center;
+ border-radius: 9px;
+ background: #1268f3;
+ color: #fff;
+ font-size: 9px;
+ line-height: 1;
+ }
+
+ .v2-filterbar > .semi-card-body:has(.v2-monitor-filter-fields.is-mobile-collapsed) {
+ gap: 0;
+ padding: 6px;
+ }
+
+ .v2-monitor-mobile-card .semi-card-body {
+ padding: 8px 9px;
+ }
+
+ .v2-monitor-card-heading-actions {
+ display: flex !important;
+ flex: 0 0 auto;
+ align-items: center;
+ flex-direction: row !important;
+ gap: 7px !important;
+ }
+
+ .v2-monitor-card-heading-actions > b {
+ color: #1f4f8d;
+ font-size: 16px;
+ font-variant-numeric: tabular-nums;
+ }
+
+ .v2-monitor-card-heading-actions > b small {
+ margin-left: 3px;
+ color: #8996a8;
+ font-size: 8px;
+ font-weight: 500;
+ }
+
+ .v2-monitor-card-heading-actions > .v2-monitor-card-locate.semi-button {
+ width: 52px;
+ height: 30px;
+ min-height: 30px;
+ border-radius: 7px;
+ padding: 0 7px;
+ font-size: 10px;
+ }
+
+ .v2-monitor-mobile-cards dl {
+ gap: 4px 9px;
+ margin-top: 6px;
+ padding-top: 6px;
+ }
+
+ .v2-monitor-mobile-cards dl > div {
+ min-height: 26px;
+ }
+
+ .v2-monitor-mobile-cards dl > div.is-address {
+ min-height: 32px;
+ }
+
+ .v2-monitor-mobile-cards dt {
+ font-size: 8px;
+ }
+
+ .v2-monitor-mobile-cards dd {
+ font-size: 10px;
+ }
+
+ .v2-monitor-mobile-cards > .v2-monitor-mobile-card.semi-card {
+ contain-intrinsic-size: auto 154px;
}
.v2-monitor-mode.v2-segmented-tabs {