feat(platform): customerize realtime map workspace

This commit is contained in:
lingniu
2026-07-05 01:16:45 +08:00
parent 83f033244c
commit 11710ec6fa
4 changed files with 362 additions and 6 deletions

View File

@@ -471,6 +471,7 @@ async function copyText(value: string, label: string) {
}
export function Realtime({
mode = 'realtime',
title = '实时监控',
description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态',
onOpenVehicle,
@@ -479,6 +480,7 @@ export function Realtime({
onFiltersChange,
initialFilters = {}
}: {
mode?: 'realtime' | 'map';
title?: string;
description?: string;
onOpenVehicle: (vin: string, protocol?: string) => void;
@@ -718,6 +720,156 @@ export function Realtime({
const selectRealtimeMapPoint = (point: VehicleMapPoint) => {
setSelectedMapPointId(point.id);
};
const mapAttentionRows = rows
.filter((row) => canOpenVehicle(row.vin) && (dataFreshness(row).stale || !isValidCoordinate(row) || hasSourceIssue(row)))
.sort((a, b) => serviceStatusWeight(b) - serviceStatusWeight(a) || String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')))
.slice(0, 8);
const mapFleetKpis = [
{ label: '车辆总数', value: pagination.total.toLocaleString(), color: 'blue' as const, helper: `当前页 ${rows.length.toLocaleString()}` },
{ label: '在线车辆', value: onlineCount.toLocaleString(), color: onlineCount > 0 ? 'green' as const : 'orange' as const, helper: `在线率 ${formatPercent(onlineRate)}` },
{ label: '有效定位', value: locatedCount.toLocaleString(), color: locatedCount > 0 ? 'green' as const : 'orange' as const, helper: `定位率 ${formatPercent(locatedRate)}` },
{ label: '需关注', value: mapAttentionRows.length.toLocaleString(), color: mapAttentionRows.length > 0 ? 'orange' as const : 'green' as const, helper: `${staleCount.toLocaleString()} 辆更新超时` }
];
if (mode === 'map') {
return (
<div className="vp-page vp-map-page">
<PageHeader title={title} description={description} />
<div className="vp-map-commandbar">
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
applyFilters(values as Record<string, string>);
}}>
<Form.Input field="keyword" label="车辆" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 160 }}>
<Select.Option value="GB32960">GB32960</Select.Option>
<Select.Option value="JT808">JT808</Select.Option>
<Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
</Form.Select>
<Form.Select field="online" label="状态" placeholder="全部车辆" style={{ width: 130 }}>
<Select.Option value="online">线</Select.Option>
<Select.Option value="offline">线</Select.Option>
</Form.Select>
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => applyFilters({})}></Button>
</Space>
</Form>
<Space wrap className="vp-map-refresh-actions">
<Tag color={autoRefresh ? 'green' : 'grey'}>{autoRefresh ? `自动刷新 ${refreshIntervalSeconds}` : '自动刷新已暂停'}</Tag>
<Typography.Text type="secondary">{lastRefreshAt || '等待首次刷新'}</Typography.Text>
<Button size="small" theme="light" loading={loading} onClick={() => load(filters, pagination.currentPage, pagination.pageSize)}></Button>
<Button size="small" theme="light" onClick={() => setAutoRefresh((value) => !value)}>{autoRefresh ? '暂停刷新' : '开启刷新'}</Button>
</Space>
</div>
<div className="vp-map-kpi-strip">
{mapFleetKpis.map((item) => (
<button
key={item.label}
type="button"
className="vp-map-kpi-item"
onClick={() => {
if (item.label === '在线车辆') applyFilters({ ...filters, online: 'online' });
if (item.label === '需关注') applyFilters({ ...filters, serviceStatus: 'degraded' });
}}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.helper}</span>
</button>
))}
</div>
<div className="vp-map-workspace">
<section className="vp-map-canvas-panel">
<div className="vp-map-canvas-toolbar">
<Space wrap>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图可用' : '坐标预览'}</Tag>
<Tag color="blue">{locatedCount.toLocaleString()} </Tag>
{filterSummary.map((item) => <Tag key={item} color="grey">{item}</Tag>)}
</Space>
<Space wrap>
<Button size="small" theme="light" onClick={exportRealtime}></Button>
<Button size="small" theme="light" disabled={!onOpenQuality} onClick={() => onOpenQuality?.({ serviceStatus: 'degraded' })}></Button>
</Space>
</div>
<VehicleMap
points={mapPoints}
maxFallbackPoints={160}
selectedId={selectedMapPointKey}
onPointSelect={selectRealtimeMapPoint}
heightClassName="vp-map-customer-canvas"
fallbackLabel="地图加载中,先显示车辆坐标态势"
/>
</section>
<aside className="vp-map-customer-side">
{selectedMapRow ? (
<div className="vp-map-customer-card vp-map-selected-card">
<div className="vp-map-card-title"></div>
<Space wrap>
<Tag color={vehicleServiceStatus(selectedMapRow).color}>{vehicleServiceStatus(selectedMapRow).label}</Tag>
<Tag color={dataFreshness(selectedMapRow).color}>{dataFreshness(selectedMapRow).detail}</Tag>
<Tag color="blue">{selectedMapRow.primaryProtocol || '未知来源'}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: '8px 0 0' }}>
{selectedMapRow.plate || selectedMapRow.vin}
</Typography.Title>
<Typography.Text type="tertiary">{selectedMapRow.vin}</Typography.Text>
<div className="vp-map-selected-metrics">
<span></span><strong>{selectedMapRow.speedKmh ?? '-'} km/h</strong>
<span>SOC</span><strong>{selectedMapRow.socPercent ?? '-'}%</strong>
<span></span><strong>{selectedMapRow.totalMileageKm ?? '-'} km</strong>
<span></span><strong>{selectedMapRow.lastSeen || '-'}</strong>
</div>
<Space wrap>
<Button size="small" theme="solid" type="primary" onClick={() => onOpenVehicle(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" onClick={() => onOpenHistory?.(selectedMapRow.vin, filters.protocol || selectedMapRow.primaryProtocol)}></Button>
<Button size="small" disabled={!isValidCoordinate(selectedMapRow)} onClick={() => window.open(amapMarkerURL(selectedMapRow), '_blank', 'noopener,noreferrer')}></Button>
</Space>
</div>
) : null}
<div className="vp-map-customer-card">
<div className="vp-map-card-title"></div>
{mapAttentionRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : mapAttentionRows.map((row) => {
const status = vehicleServiceStatus(row);
return (
<button key={`${row.vin}-${row.primaryProtocol}-map-attention`} type="button" className="vp-map-attention-row" onClick={() => selectRealtimeRow(row)}>
<div>
<Typography.Text strong>{row.plate || row.vin}</Typography.Text>
<Typography.Text type="tertiary" size="small">{row.vin}</Typography.Text>
</div>
<Tag color={status.color}>{status.label}</Tag>
<Typography.Text type="secondary" size="small">{realtimeIssueLabels(row).join('')}</Typography.Text>
</button>
);
})}
</div>
<div className="vp-map-customer-card">
<div className="vp-map-card-title"></div>
{mapServiceRows.length === 0 ? (
<Typography.Text type="tertiary"></Typography.Text>
) : mapServiceRows.map((row) => (
<button
key={`${row.vin}-${row.primaryProtocol}-map-list`}
type="button"
className={`vp-map-vehicle-row ${selectedMapRow?.vin === row.vin ? 'vp-map-vehicle-row-selected' : ''}`}
onClick={() => selectRealtimeRow(row)}
>
<span>{row.plate || row.vin}</span>
<Tag color={dataFreshness(row).color}>{dataFreshness(row).label}</Tag>
</button>
))}
</div>
</aside>
</div>
</div>
);
}
return (
<div className="vp-page">