feat(platform): customerize realtime map workspace
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user