feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
This commit is contained in:
@@ -23,7 +23,7 @@ python3 server.py
|
||||
|
||||
## 数据访问
|
||||
|
||||
- `GET /api/dashboard` 公开返回车辆信息、站点基础信息及加氢量。
|
||||
- `GET /api/dashboard` 公开返回车辆信息、站点基础信息;站点及汇总均不返回本月或累计加氢量。
|
||||
- 不保存访问码、不创建浏览器会话,也不区分公开或受保护的数据接口。
|
||||
|
||||
## 接口依赖
|
||||
|
||||
+17
-29
@@ -4,7 +4,7 @@ const i18n = {
|
||||
zh: {
|
||||
vehicleModeTitle: '车辆网络', stationModeTitle: '加氢站网络',
|
||||
kpiTotalFleet: '车辆总数', kpiTotalStation: '站点总数',
|
||||
kpiOnlineFleet: '今日上线车辆', kpiCooperativeStation: '合作站点', kpiDailyMileage: '今日运营里程', kpiMonthlyHydrogen: '本月加氢量',
|
||||
kpiOnlineFleet: '今日上线车辆', kpiCooperativeStation: '合作站点', kpiDailyMileage: '今日运营里程',
|
||||
unitVehicle: '辆', unitVehicles: '辆', unitStation: '座', unitStations: '座', btnVehicle: '车辆', btnStation: '加氢站',
|
||||
themeDark: '深色', themeLight: '浅色', legendPrimary: '核心节点 (>100)',
|
||||
legendMid: '重点节点 (20-100)', legendLow: '普通节点 (<20)', btnResetView: '复位视角',
|
||||
@@ -15,14 +15,14 @@ const i18n = {
|
||||
panelRankVehicle: '车辆区域分布 TOP 排名', panelRankStation: '加氢站省份 TOP 排名',
|
||||
statusTotal: '总数', statusRunning: '行驶中', statusStopped: '静止中', statusOffline: '离线',
|
||||
stationCooperative: '合作站', stationExternal: '外部站', stationTotal: '全部站点', stationPartnerMarker: '合作',
|
||||
rankByFleet: '按数量', rankByDist: '按里程', rankByStations: '按站点数', rankByHydrogen: '按加氢量', rankNoHydrogenData: '当前视野暂无加氢数据',
|
||||
rankByFleet: '按数量', rankByDist: '按里程', rankByStations: '按站点数',
|
||||
stationViewportEmpty: '当前视野暂无加氢站 · 可拖动地图或复位视角', viewportNow: '当前视野', viewportAll: '全部',
|
||||
onlineText: '在线', unitTail: '台', btnLocate: '定位', locating: '定位中', located: '已定位', locateFailed: '定位失败',
|
||||
btnClearFilters: '清除', filterAllProvince: '全部省份', filterAllCity: '全部城市', filterAllDistrict: '全部区县',
|
||||
filterResult: '筛选', searchVehicle: '搜索车牌 / VIN', searchStation: '搜索站点名称 / 地址', nearbyVehicles: '附近车辆', nearbyStations: '附近加氢站',
|
||||
detailVehicle: '车辆', detailStation: '加氢站', detailPlate: '车牌', detailVin: 'VIN', detailStatus: '车辆状态', detailActive: '今日上线',
|
||||
detailSpeed: '当前速度', detailDailyMileage: '今日里程', detailTotalMileage: '累计里程', detailLocation: '实时位置', detailCoordinate: '坐标',
|
||||
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailContactPerson: '联系人', detailContactPhone: '联系方式', detailUnitPrice: '单价', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量', detailNavigate: '导航',
|
||||
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailContactPerson: '联系人', detailContactPhone: '联系方式', detailUnitPrice: '单价', detailNavigate: '导航',
|
||||
navigationLaunchingTitle: '正在打开高德地图', navigationLaunchingDescription: '正在准备导航路线,请稍候',
|
||||
valueYes: '是', valueNo: '否', valueUnavailable: '暂无数据', locatePermissionHint: '无法获取位置,请在浏览器中允许位置权限后重试',
|
||||
locateUnavailableHint: '暂时无法获取设备位置,请稍后重试', locateTimeoutHint: '定位超时,请到开阔区域后重试',
|
||||
@@ -32,7 +32,7 @@ const i18n = {
|
||||
en: {
|
||||
vehicleModeTitle: 'Vehicle Network', stationModeTitle: 'H₂ Station Network',
|
||||
kpiTotalFleet: 'Total Fleet', kpiTotalStation: 'Total Stations',
|
||||
kpiOnlineFleet: 'Active Today', kpiCooperativeStation: 'Partner Stations', kpiDailyMileage: 'Today Mileage', kpiMonthlyHydrogen: 'Monthly Hydrogen Fill',
|
||||
kpiOnlineFleet: 'Active Today', kpiCooperativeStation: 'Partner Stations', kpiDailyMileage: 'Today Mileage',
|
||||
unitVehicle: 'unit', unitVehicles: 'units', unitStation: 'station', unitStations: 'stations', btnVehicle: 'Fleet', btnStation: 'H₂ Stations',
|
||||
themeDark: 'Dark', themeLight: 'Light', legendPrimary: 'Core (>100)',
|
||||
legendMid: 'Key (20-100)', legendLow: 'Standard (<20)', btnResetView: 'Reset View',
|
||||
@@ -43,14 +43,14 @@ const i18n = {
|
||||
panelRankVehicle: 'Top Fleet Regions', panelRankStation: 'Top Provinces by Stations',
|
||||
statusTotal: 'Total', statusRunning: 'Driving', statusStopped: 'Idle', statusOffline: 'Offline',
|
||||
stationCooperative: 'Partners', stationExternal: 'External', stationTotal: 'All Stations', stationPartnerMarker: 'Partner',
|
||||
rankByFleet: 'By Count', rankByDist: 'By Mileage', rankByStations: 'By Stations', rankByHydrogen: 'By H₂ Volume', rankNoHydrogenData: 'No hydrogen data in this view',
|
||||
rankByFleet: 'By Count', rankByDist: 'By Mileage', rankByStations: 'By Stations',
|
||||
stationViewportEmpty: 'No H₂ stations in this view · Pan the map or reset view', viewportNow: 'In view', viewportAll: 'All',
|
||||
onlineText: 'Online', unitTail: 'units', btnLocate: 'Locate', locating: 'Locating', located: 'Located', locateFailed: 'Location failed',
|
||||
btnClearFilters: 'Clear', filterAllProvince: 'All provinces', filterAllCity: 'All cities', filterAllDistrict: 'All districts',
|
||||
filterResult: 'Filtered', searchVehicle: 'Search plate / VIN', searchStation: 'Search station / address', nearbyVehicles: 'Nearby vehicles', nearbyStations: 'Nearby H₂ stations',
|
||||
detailVehicle: 'Vehicle', detailStation: 'H₂ station', detailPlate: 'Plate', detailVin: 'VIN', detailStatus: 'Status', detailActive: 'Active today',
|
||||
detailSpeed: 'Speed', detailDailyMileage: 'Today mileage', detailTotalMileage: 'Total mileage', detailLocation: 'Live location', detailCoordinate: 'Coordinates',
|
||||
detailStationType: 'Station type', detailAdmin: 'Region', detailAddress: 'Address', detailContactPerson: 'Contact', detailContactPhone: 'Phone', detailUnitPrice: 'Unit price', detailMonthlyHydrogen: 'Monthly hydrogen', detailTotalHydrogen: 'Total hydrogen', detailNavigate: 'Navigate',
|
||||
detailStationType: 'Station type', detailAdmin: 'Region', detailAddress: 'Address', detailContactPerson: 'Contact', detailContactPhone: 'Phone', detailUnitPrice: 'Unit price', detailNavigate: 'Navigate',
|
||||
navigationLaunchingTitle: 'Opening AMap', navigationLaunchingDescription: 'Preparing your route. Just a moment.',
|
||||
valueYes: 'Yes', valueNo: 'No', valueUnavailable: 'Unavailable', locatePermissionHint: 'Location access is blocked. Allow it in your browser and try again.',
|
||||
locateUnavailableHint: 'Your location is temporarily unavailable. Please try again.', locateTimeoutHint: 'Location timed out. Move to an open area and try again.',
|
||||
@@ -762,9 +762,8 @@ function updateDashboardUI() {
|
||||
document.getElementById('kpiActiveLabel').textContent = kpiLabels.active;
|
||||
document.getElementById('kpiFleetTotal').innerHTML = `${formatNumber(total)} <small>${unit}</small>`;
|
||||
document.getElementById('kpiFleetOnline').innerHTML = `${formatNumber(active)} <small>${countUnit(active, currentMode, dict)} (${activeRate}%)</small>`;
|
||||
const activity = currentMode === 'vehicle'
|
||||
? { label: dict.kpiDailyMileage, value: summary.todayMileageKm, unit: 'km' }
|
||||
: { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' };
|
||||
document.getElementById('kpiActivityLabel').parentElement.style.display = currentMode === 'vehicle' ? '' : 'none';
|
||||
const activity = { label: dict.kpiDailyMileage, value: summary.todayMileageKm, unit: 'km' };
|
||||
document.getElementById('kpiActivityLabel').textContent = activity.label;
|
||||
document.getElementById('kpiDailyDist').innerHTML = activity.value == null
|
||||
? `—${activity.unit ? ` <small>${activity.unit}</small>` : ''}`
|
||||
@@ -1163,8 +1162,7 @@ function buildStationNodes(stations = filteredStations(), level = stationHierarc
|
||||
count: 1, online: station.cooperative ? 1 : 0,
|
||||
adminPath: [stationAdministrativePath(station), stationDistrictName(station)].filter(Boolean).filter((value, index, items) => index === 0 || !items[0].includes(value)).join(' · '),
|
||||
detail: [stationAdministrativePath(station), currentLang === 'zh' ? station.address : ''].filter(Boolean).join(' · '),
|
||||
cooperative: station.cooperative, hydrogenKg: Number(station.totalHydrogenKg) || 0,
|
||||
hasHydrogen: station.totalHydrogenKg != null
|
||||
cooperative: station.cooperative
|
||||
}));
|
||||
}
|
||||
const grouped = new Map();
|
||||
@@ -1172,15 +1170,11 @@ function buildStationNodes(stations = filteredStations(), level = stationHierarc
|
||||
const key = (level === 'province' ? station.province : `${station.province || ''}/${station.city || ''}`) || 'UNSPECIFIED';
|
||||
if (!grouped.has(key)) grouped.set(key, {
|
||||
kind: level === 'province' ? 'stationProvince' : 'stationCity', name: stationGroupName(station, level),
|
||||
lng: 0, lat: 0, count: 0, online: 0, hydrogenKg: 0, hasHydrogen: false, stations: []
|
||||
lng: 0, lat: 0, count: 0, online: 0, stations: []
|
||||
});
|
||||
const group = grouped.get(key);
|
||||
group.lng += Number(station.longitude); group.lat += Number(station.latitude); group.count += 1;
|
||||
group.online += station.cooperative ? 1 : 0; group.stations.push(station);
|
||||
if (station.totalHydrogenKg != null) {
|
||||
group.hasHydrogen = true;
|
||||
group.hydrogenKg += Number(station.totalHydrogenKg) || 0;
|
||||
}
|
||||
}
|
||||
return [...grouped.values()].map((group, index) => ({
|
||||
...group, id: `station-${level}-${index}`, lnglat: [group.lng / group.count, group.lat / group.count],
|
||||
@@ -1363,11 +1357,7 @@ function showEntityDetails(mode, entity) {
|
||||
detailField(dict.detailAddress, detailValue(entity.address), true),
|
||||
detailField(dict.detailCoordinate, `${formatNumber(entity.longitude, 6)}, ${formatNumber(entity.latitude, 6)}`, true, true)
|
||||
];
|
||||
const hydrogenFields = [
|
||||
detailField(dict.detailMonthlyHydrogen, entity.monthlyHydrogenKg == null ? dict.valueUnavailable : `${formatNumber(entity.monthlyHydrogenKg, 1)} kg`),
|
||||
detailField(dict.detailTotalHydrogen, entity.totalHydrogenKg == null ? dict.valueUnavailable : `${formatNumber(entity.totalHydrogenKg, 1)} kg`, false, true),
|
||||
];
|
||||
grid.innerHTML = [...publicFields, ...hydrogenFields].join('');
|
||||
grid.innerHTML = publicFields.join('');
|
||||
} else {
|
||||
title.textContent = entity.plateNumber || entity.vin || dict.detailVehicle;
|
||||
subtitle.textContent = entity.vin || '';
|
||||
@@ -1496,16 +1486,14 @@ function renderRankingList(type) {
|
||||
}
|
||||
} else {
|
||||
const { level, allNodes, visibleNodes } = stationViewportSummary();
|
||||
const byHydrogen = type === 'secondary';
|
||||
rows = [...visibleNodes]
|
||||
.filter(node => !byHydrogen || node.hasHydrogen)
|
||||
.sort((a, b) => userLocation && level === 'station'
|
||||
? distanceKm(userLocation, a.lnglat) - distanceKm(userLocation, b.lnglat)
|
||||
: byHydrogen ? b.hydrogenKg - a.hydrogenKg : b.count - a.count || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
: b.count - a.count || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
.map(node => ({
|
||||
name: node.name,
|
||||
value: userLocation && level === 'station' ? `${formatNumber(distanceKm(userLocation, node.lnglat), 1)} km`
|
||||
: byHydrogen ? `${formatNumber(node.hydrogenKg, 1)} kg` : countLabel(node.count, 'station'),
|
||||
: countLabel(node.count, 'station'),
|
||||
price: level === 'station' && node.station?.cooperative ? stationUnitPrice(node.station, '') : '',
|
||||
location: node.lnglat,
|
||||
zoom: level === 'province' ? 7.2 : level === 'city' ? 11.2 : 14,
|
||||
@@ -1513,7 +1501,7 @@ function renderRankingList(type) {
|
||||
}));
|
||||
updateStationViewportMeta(visibleNodes.length, allNodes.length);
|
||||
if (!rows.length) {
|
||||
const message = byHydrogen && visibleNodes.length ? i18n[currentLang].rankNoHydrogenData : i18n[currentLang].stationViewportEmpty;
|
||||
const message = i18n[currentLang].stationViewportEmpty;
|
||||
box.innerHTML = `<div class="rank-empty rank-empty-viewport"><span class="rank-empty-icon">⌖</span><span>${escapeHTML(message)}</span></div>`;
|
||||
}
|
||||
}
|
||||
@@ -1554,9 +1542,9 @@ function updateRankingControls() {
|
||||
document.getElementById('panelRankTitle').textContent = title;
|
||||
if (currentMode === 'vehicle') updateStationViewportMeta(0, 0);
|
||||
document.getElementById('rankPrimaryTab').textContent = currentMode === 'vehicle' ? dict.rankByFleet : dict.rankByStations;
|
||||
document.getElementById('rankSecondaryTab').textContent = currentMode === 'vehicle'
|
||||
? dict.rankByDist
|
||||
: dict.rankByHydrogen;
|
||||
if (currentMode === 'station') currentRankType = 'primary';
|
||||
document.getElementById('rankSecondaryTab').style.display = currentMode === 'vehicle' ? '' : 'none';
|
||||
document.getElementById('rankSecondaryTab').textContent = dict.rankByDist;
|
||||
document.querySelectorAll('.gtab').forEach(button => button.classList.toggle('active', button.id === (currentRankType === 'primary' ? 'rankPrimaryTab' : 'rankSecondaryTab')));
|
||||
}
|
||||
|
||||
|
||||
@@ -372,7 +372,10 @@ def _load_dashboard():
|
||||
stations_future = executor.submit(_load_stations)
|
||||
vehicles = realtime_future.result()
|
||||
mileage_rows = mileage_future.result()
|
||||
stations = stations_future.result()
|
||||
stations = [
|
||||
{key: value for key, value in station.items() if key not in ("monthlyHydrogenKg", "totalHydrogenKg")}
|
||||
for station in stations_future.result()
|
||||
]
|
||||
|
||||
mileage_by_vin = {
|
||||
row.get("vin"): row
|
||||
@@ -389,9 +392,6 @@ def _load_dashboard():
|
||||
daily_mileage = round(
|
||||
sum(float(row.get("dailyMileageKm") or 0) for row in mileage_rows), 3
|
||||
)
|
||||
monthly_hydrogen = round(
|
||||
sum(float(station.get("monthlyHydrogenKg") or 0) for station in stations), 3
|
||||
)
|
||||
for vehicle in vehicles:
|
||||
mileage = mileage_by_vin.get(vehicle.get("vin"), {})
|
||||
vehicle["dailyMileageKm"] = mileage.get("dailyMileageKm", 0)
|
||||
@@ -407,7 +407,6 @@ def _load_dashboard():
|
||||
"idleVehicles": idle,
|
||||
"offlineVehicles": offline,
|
||||
"todayMileageKm": daily_mileage,
|
||||
"monthlyHydrogenKg": monthly_hydrogen,
|
||||
"totalStations": len(stations),
|
||||
"cooperativeStations": sum(1 for item in stations if item.get("cooperative")),
|
||||
},
|
||||
|
||||
@@ -28,13 +28,17 @@ class DashboardTest(unittest.TestCase):
|
||||
{"vin": "VIN3", "status": "NO_DATA", "dailyMileageKm": None},
|
||||
]
|
||||
if path.endswith("hydrogen-stations/query"):
|
||||
return [{"id": "1", "cooperative": True}, {"id": "2", "cooperative": False}]
|
||||
return [{"id": "1", "cooperative": True, "monthlyHydrogenKg": 123.4, "totalHydrogenKg": 567.8}, {"id": "2", "cooperative": False, "monthlyHydrogenKg": 0, "totalHydrogenKg": 0}]
|
||||
raise AssertionError(path)
|
||||
|
||||
with patch.object(server, "_post_open_platform", side_effect=fake_post):
|
||||
with patch.object(server, "_cache", {}):
|
||||
result = server._load_dashboard()
|
||||
|
||||
self.assertNotIn("totalHydrogenKg", result["summary"])
|
||||
self.assertTrue(all("totalHydrogenKg" not in station for station in result["stations"]))
|
||||
self.assertNotIn("monthlyHydrogenKg", result["summary"])
|
||||
self.assertTrue(all("monthlyHydrogenKg" not in station for station in result["stations"]))
|
||||
self.assertEqual(result["summary"]["totalVehicles"], 3)
|
||||
self.assertEqual(result["summary"]["onlineVehicles"], 2)
|
||||
self.assertEqual(result["summary"]["drivingVehicles"], 1)
|
||||
|
||||
Reference in New Issue
Block a user