feat(map): add protected operations access
This commit is contained in:
+263
-20
@@ -22,11 +22,15 @@ const i18n = {
|
||||
filterResult: '筛选', searchVehicle: '搜索车牌 / VIN', searchStation: '搜索站点名称 / 地址', nearbyVehicles: '附近车辆', nearbyStations: '附近加氢站',
|
||||
detailVehicle: '车辆', detailStation: '加氢站', detailPlate: '车牌', detailVin: 'VIN', detailStatus: '车辆状态', detailActive: '今日上线',
|
||||
detailSpeed: '当前速度', detailDailyMileage: '今日里程', detailTotalMileage: '累计里程', detailLocation: '实时位置', detailCoordinate: '坐标',
|
||||
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量',
|
||||
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量', detailNavigate: '导航',
|
||||
valueYes: '是', valueNo: '否', valueUnavailable: '暂无数据', locatePermissionHint: '无法获取位置,请在浏览器中允许位置权限后重试',
|
||||
locateUnavailableHint: '暂时无法获取设备位置,请稍后重试', locateTimeoutHint: '定位超时,请到开阔区域后重试',
|
||||
searchUniversal: '搜索位置、站点、车牌或 VIN', searchLocations: '位置', searchEntities: '业务对象', searchEmpty: '没有匹配的地点或对象',
|
||||
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息'
|
||||
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息',
|
||||
publicStationTitle: '加氢站网络', publicDataStatus: '公共站点目录 · 位置、地址与合作状态', publicDataSource: '公共站点网络',
|
||||
authEntry: '运营授权', authGranted: '已授权', authTitle: '进入运营数据', authDescription: '车辆全量信息与加氢量仅向获得授权的用户展示。',
|
||||
authCodeLabel: '访问码', authCodePlaceholder: '输入访问码', authSubmit: '验证并进入', authCancel: '暂不授权', authRequired: '运营数据需要授权访问', authExpiry: '本次授权有效期为 30 分钟,到期后将自动恢复为公开站点视图。',
|
||||
authDenied: '访问码无效,请重试', authUnavailable: '授权服务暂不可用,请稍后重试', authHydrogenRequired: '授权后查看加氢量', publicHydrogenHidden: '运营数据受保护', authLogout: '退出授权'
|
||||
},
|
||||
en: {
|
||||
vehicleModeTitle: 'Vehicle Network', stationModeTitle: 'H₂ Station Network',
|
||||
@@ -49,11 +53,15 @@ const i18n = {
|
||||
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', detailMonthlyHydrogen: 'Monthly hydrogen', detailTotalHydrogen: 'Total hydrogen',
|
||||
detailStationType: 'Station type', detailAdmin: 'Region', detailAddress: 'Address', detailMonthlyHydrogen: 'Monthly hydrogen', detailTotalHydrogen: 'Total hydrogen', detailNavigate: 'Navigate',
|
||||
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.',
|
||||
searchUniversal: 'Search location, station, plate, or VIN', searchLocations: 'Locations', searchEntities: 'Results', searchEmpty: 'No matching location or entity',
|
||||
suggestionLocation: 'Location', suggestionVehicle: 'Vehicle', suggestionStation: 'H₂ station', detailMore: 'Show all details', detailLess: 'Collapse details'
|
||||
suggestionLocation: 'Location', suggestionVehicle: 'Vehicle', suggestionStation: 'H₂ station', detailMore: 'Show all details', detailLess: 'Collapse details',
|
||||
publicStationTitle: 'H₂ Station Network', publicDataStatus: 'Public station directory · location, address and partner status', publicDataSource: 'Public station network',
|
||||
authEntry: 'Operations access', authGranted: 'Access granted', authTitle: 'Enter operations data', authDescription: 'Vehicle information and hydrogen volumes require authorized access.',
|
||||
authCodeLabel: 'Access code', authCodePlaceholder: 'Enter access code', authSubmit: 'Verify and continue', authCancel: 'Not now', authRequired: 'Operations data requires authorized access', authExpiry: 'This authorization lasts 30 minutes. When it expires, the public station view is restored.',
|
||||
authDenied: 'The access code is invalid. Try again.', authUnavailable: 'Authorization is temporarily unavailable. Try again later.', authHydrogenRequired: 'Authorize to view hydrogen volume', publicHydrogenHidden: 'Operations data protected', authLogout: 'Sign out'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +96,7 @@ let map = null;
|
||||
let markerList = [];
|
||||
let infoWindowList = [];
|
||||
let is3DPitch = true;
|
||||
let currentMode = 'vehicle';
|
||||
let currentMode = 'station';
|
||||
let currentTheme = 'theme-white';
|
||||
let currentLang = 'zh';
|
||||
let currentRankType = 'primary';
|
||||
@@ -109,6 +117,7 @@ let exploreSuggestionItems = [];
|
||||
let activeExploreSuggestionIndex = -1;
|
||||
let detailExpanded = false;
|
||||
const filterState = { province: '', city: '', district: '', query: '' };
|
||||
let accessState = { authorized: false, principal: null, pendingMode: null };
|
||||
|
||||
const REGION_ZOOM = { city: 7, district: 9.5, vehicle: 12 };
|
||||
const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
|
||||
@@ -116,27 +125,75 @@ const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initClock();
|
||||
initAMapInstance();
|
||||
loadDashboard();
|
||||
refreshTimer = window.setInterval(loadDashboard, 15000);
|
||||
initializeDataAccess();
|
||||
});
|
||||
|
||||
document.addEventListener('pointerdown', event => {
|
||||
if (!event.target?.closest?.('.map-explore-toolbar')) closeExploreSuggestions();
|
||||
});
|
||||
|
||||
async function loadDashboard() {
|
||||
async function initializeDataAccess() {
|
||||
await loadSession();
|
||||
await loadPublicStations();
|
||||
if (accessState.authorized) loadOperationalDashboard();
|
||||
refreshTimer = window.setInterval(() => {
|
||||
if (accessState.authorized) loadOperationalDashboard();
|
||||
else loadPublicStations();
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
async function loadSession() {
|
||||
try {
|
||||
const response = await fetch('/api/session', { headers: { Accept: 'application/json' } });
|
||||
const payload = await response.json();
|
||||
accessState.authorized = Boolean(payload.authorized);
|
||||
accessState.principal = payload.principal || null;
|
||||
} catch (error) {
|
||||
accessState.authorized = false;
|
||||
accessState.principal = null;
|
||||
}
|
||||
updateAccessUI();
|
||||
}
|
||||
|
||||
async function loadPublicStations() {
|
||||
setDataState('loading');
|
||||
try {
|
||||
const response = await fetch('/api/public/stations', { headers: { Accept: 'application/json' } });
|
||||
const payload = await response.json();
|
||||
if (!response.ok || payload.status !== 'ok') throw new Error(payload.message || `HTTP ${response.status}`);
|
||||
if (!accessState.authorized || currentMode === 'station') dashboard = payload;
|
||||
void syncFilterControls(payload);
|
||||
updateDashboardUI();
|
||||
if (currentMode === 'station') refreshStationViewport();
|
||||
setDataState('ready');
|
||||
} catch (error) {
|
||||
console.error('public station refresh failed', error);
|
||||
setDataState('error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOperationalDashboard() {
|
||||
setDataState('loading');
|
||||
try {
|
||||
const response = await fetch('/api/dashboard', { headers: { Accept: 'application/json' } });
|
||||
const payload = await response.json();
|
||||
if (response.status === 401) {
|
||||
accessState.authorized = false;
|
||||
accessState.principal = null;
|
||||
updateAccessUI();
|
||||
if (currentMode === 'vehicle') switchMode('station');
|
||||
await loadPublicStations();
|
||||
return;
|
||||
}
|
||||
if (!response.ok || payload.status !== 'ok') throw new Error(payload.message || `HTTP ${response.status}`);
|
||||
dashboard = payload;
|
||||
void syncFilterControls(payload);
|
||||
updateDashboardUI();
|
||||
refreshVehicleRegionNodes(payload);
|
||||
if (currentMode === 'vehicle') refreshVehicleRegionNodes(payload);
|
||||
else refreshStationViewport();
|
||||
setDataState('ready');
|
||||
} catch (error) {
|
||||
console.error('dashboard refresh failed', error);
|
||||
console.error('operational dashboard refresh failed', error);
|
||||
setDataState('error');
|
||||
}
|
||||
}
|
||||
@@ -148,6 +205,97 @@ function setDataState(state) {
|
||||
if (state === 'error') el.textContent = currentLang === 'zh' ? '数据同步失败 · 将自动重试' : 'Data sync failed · Retrying';
|
||||
}
|
||||
|
||||
function operationalAccessGranted() {
|
||||
return Boolean(accessState.authorized);
|
||||
}
|
||||
|
||||
function updateAccessUI() {
|
||||
const dict = i18n[currentLang];
|
||||
const trigger = document.getElementById('accessTrigger');
|
||||
if (trigger) {
|
||||
trigger.classList.toggle('is-authorized', operationalAccessGranted());
|
||||
trigger.textContent = operationalAccessGranted() ? dict.authGranted : dict.authEntry;
|
||||
trigger.setAttribute('aria-label', operationalAccessGranted() ? dict.authLogout : dict.authEntry);
|
||||
trigger.title = operationalAccessGranted() ? dict.authLogout : dict.authEntry;
|
||||
}
|
||||
document.getElementById('btnModeVehicle')?.classList.toggle('is-locked', !operationalAccessGranted());
|
||||
}
|
||||
|
||||
function openAccessDialog(targetMode = 'vehicle', message = '') {
|
||||
accessState.pendingMode = targetMode;
|
||||
const dict = i18n[currentLang];
|
||||
const dialog = document.getElementById('accessDialog');
|
||||
const hint = document.getElementById('accessDialogHint');
|
||||
const input = document.getElementById('accessCodeInput');
|
||||
if (hint) hint.textContent = message || dict.authDescription;
|
||||
if (input) input.value = '';
|
||||
if (dialog?.showModal) dialog.showModal();
|
||||
else dialog?.setAttribute('open', '');
|
||||
window.setTimeout(() => input?.focus(), 0);
|
||||
}
|
||||
|
||||
function closeAccessDialog() {
|
||||
const dialog = document.getElementById('accessDialog');
|
||||
if (dialog?.close) dialog.close();
|
||||
else dialog?.removeAttribute('open');
|
||||
const error = document.getElementById('accessDialogError');
|
||||
if (error) { error.hidden = true; error.textContent = ''; }
|
||||
}
|
||||
|
||||
async function authorizeOperationsAccess() {
|
||||
const dict = i18n[currentLang];
|
||||
const input = document.getElementById('accessCodeInput');
|
||||
const submit = document.getElementById('accessSubmitBtn');
|
||||
const error = document.getElementById('accessDialogError');
|
||||
const accessCode = input?.value?.trim();
|
||||
if (!accessCode) {
|
||||
if (error) { error.hidden = false; error.textContent = dict.authDenied; }
|
||||
input?.focus();
|
||||
return;
|
||||
}
|
||||
if (submit) submit.disabled = true;
|
||||
if (error) error.hidden = true;
|
||||
try {
|
||||
const response = await fetch('/api/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ accessCode }),
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok || !payload.authorized) {
|
||||
if (error) { error.hidden = false; error.textContent = response.status === 503 ? dict.authUnavailable : dict.authDenied; }
|
||||
return;
|
||||
}
|
||||
accessState.authorized = true;
|
||||
accessState.principal = payload.principal || null;
|
||||
updateAccessUI();
|
||||
closeAccessDialog();
|
||||
await loadOperationalDashboard();
|
||||
const targetMode = accessState.pendingMode || 'vehicle';
|
||||
accessState.pendingMode = null;
|
||||
if (targetMode === 'vehicle') switchMode('vehicle', { skipAuthCheck: true });
|
||||
else updateDashboardUI();
|
||||
} catch (requestError) {
|
||||
if (error) { error.hidden = false; error.textContent = dict.authUnavailable; }
|
||||
} finally {
|
||||
if (submit) submit.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleOperationsAccess() {
|
||||
if (!operationalAccessGranted()) {
|
||||
openAccessDialog('vehicle');
|
||||
return;
|
||||
}
|
||||
try { await fetch('/api/session', { method: 'DELETE', headers: { Accept: 'application/json' } }); } catch (error) { /* local state still resets safely */ }
|
||||
accessState.authorized = false;
|
||||
accessState.principal = null;
|
||||
accessState.pendingMode = null;
|
||||
updateAccessUI();
|
||||
if (currentMode === 'vehicle') switchMode('station', { skipAuthCheck: true });
|
||||
await loadPublicStations();
|
||||
}
|
||||
|
||||
function normalizeSearch(value) {
|
||||
return String(value || '').trim().toLocaleLowerCase();
|
||||
}
|
||||
@@ -181,6 +329,10 @@ function selectedVehicleFilterGroup() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasPinnedVehicleDistrictFilter() {
|
||||
return currentMode === 'vehicle' && Boolean(filterState.district) && !normalizeSearch(filterState.query);
|
||||
}
|
||||
|
||||
function filteredVehicles(vehicles = dashboard?.vehicles || []) {
|
||||
const group = selectedVehicleFilterGroup();
|
||||
const allowedVins = group ? new Set(group.vehicles.map(vehicle => vehicle.vin)) : null;
|
||||
@@ -572,10 +724,40 @@ function focusCoordinates(items, zoom) {
|
||||
map.setZoomAndCenter(zoom, [center[0] / located.length, center[1] / located.length]);
|
||||
}
|
||||
|
||||
function focusVehicleSet(vehicles) {
|
||||
const points = locatedVehicles(vehicles).map(vehicle => wgs84ToGcj02(Number(vehicle.longitude), Number(vehicle.latitude)));
|
||||
if (!map || !points.length) return;
|
||||
if (points.length === 1) {
|
||||
map.setZoomAndCenter(14, points[0]);
|
||||
return;
|
||||
}
|
||||
const longitudes = points.map(point => point[0]);
|
||||
const latitudes = points.map(point => point[1]);
|
||||
const minLng = Math.min(...longitudes); const maxLng = Math.max(...longitudes);
|
||||
const minLat = Math.min(...latitudes); const maxLat = Math.max(...latitudes);
|
||||
const center = [(minLng + maxLng) / 2, (minLat + maxLat) / 2];
|
||||
const latitudeScale = Math.cos(center[1] * Math.PI / 180);
|
||||
const spanMeters = Math.max((maxLat - minLat) * 111320, (maxLng - minLng) * 111320 * latitudeScale, 400);
|
||||
const size = map.getSize?.();
|
||||
const width = Number(size?.getWidth?.()) || 900;
|
||||
const height = Number(size?.getHeight?.()) || 560;
|
||||
// Reserve room for the toolbar, detail card and map controls on every viewport.
|
||||
const usablePixels = Math.max(180, Math.min(width * 0.72, height * 0.68));
|
||||
const metersPerPixel = spanMeters / usablePixels;
|
||||
const zoom = Math.max(7, Math.min(14, Math.log2((156543.03392 * latitudeScale) / metersPerPixel)));
|
||||
map.setZoomAndCenter(zoom, center);
|
||||
}
|
||||
|
||||
function focusCurrentRegion(level) {
|
||||
if (!map) return;
|
||||
if (currentMode === 'vehicle') {
|
||||
const group = selectedVehicleFilterGroup();
|
||||
if (level === 'district' && hasPinnedVehicleDistrictFilter() && locatedVehicles(filteredVehicles()).length) {
|
||||
// District borders can be elongated. Fit the actual filtered fleet rather
|
||||
// than centering on the administrative centroid, which can hide a valid car.
|
||||
focusVehicleSet(filteredVehicles());
|
||||
return;
|
||||
}
|
||||
if (group?.lnglat) map.setZoomAndCenter(level === 'province' ? 7.4 : level === 'city' ? 10 : 12.5, group.lnglat);
|
||||
return;
|
||||
}
|
||||
@@ -682,10 +864,12 @@ function updateDashboardUI() {
|
||||
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' };
|
||||
: operationalAccessGranted()
|
||||
? { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' }
|
||||
: { label: dict.publicHydrogenHidden, value: null, unit: '' };
|
||||
document.getElementById('kpiActivityLabel').textContent = activity.label;
|
||||
document.getElementById('kpiDailyDist').innerHTML = activity.value == null
|
||||
? `— <small>${activity.unit}</small>`
|
||||
? `—${activity.unit ? ` <small>${activity.unit}</small>` : ''}`
|
||||
: `${formatNumber(activity.value, 1)} <small>${activity.unit}</small>`;
|
||||
const regionSummary = buildVehicleRegionNodes();
|
||||
document.getElementById('mapStatusText').textContent = mapStatusSummaryText(summary, regionSummary, dashboard.asOf);
|
||||
@@ -700,9 +884,12 @@ function mapStatusSummaryText(summary, regionSummary, asOf, mode = currentMode,
|
||||
if (mode === 'station') {
|
||||
const cooperative = Number(summary.cooperativeStations || 0);
|
||||
const external = Math.max(0, Number(summary.totalStations || 0) - cooperative);
|
||||
const accessHint = operationalAccessGranted()
|
||||
? ''
|
||||
: lang === 'zh' ? ' · 站点目录公开展示' : ' · public station directory';
|
||||
return lang === 'zh'
|
||||
? `开放平台已同步 · ${summary.totalStations}座加氢站 · ${cooperative}座合作站点 · ${external}座外部站点 · ${asOf}`
|
||||
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations · ${asOf}`;
|
||||
? `开放平台已同步 · ${summary.totalStations}座加氢站 · ${cooperative}座合作站点 · ${external}座外部站点${accessHint} · ${asOf}`
|
||||
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations${accessHint} · ${asOf}`;
|
||||
}
|
||||
const regionText = regionSummary.loading ? regionLoadingText(regionSummary.level, lang) : regionSummaryText(regionSummary, lang);
|
||||
return lang === 'zh'
|
||||
@@ -818,6 +1005,7 @@ function stationAdministrativePath(station, lang = currentLang) {
|
||||
}
|
||||
|
||||
function hierarchyLevelForZoom(zoom = map?.getZoom?.() || 0) {
|
||||
if (hasPinnedVehicleDistrictFilter()) return 'vehicle';
|
||||
if (zoom < REGION_ZOOM.city) return 'province';
|
||||
if (zoom < REGION_ZOOM.district) return 'city';
|
||||
if (zoom < REGION_ZOOM.vehicle) return 'district';
|
||||
@@ -1005,7 +1193,7 @@ function vehiclePointNodes(vehicles = filteredVehicles()) {
|
||||
return locatedVehicles(vehicles).map(vehicle => {
|
||||
const lnglat = wgs84ToGcj02(Number(vehicle.longitude), Number(vehicle.latitude));
|
||||
return { vehicle, lnglat };
|
||||
}).filter(({ lnglat }) => !bounds || (lnglat[0] >= bounds.west && lnglat[0] <= bounds.east && lnglat[1] >= bounds.south && lnglat[1] <= bounds.north))
|
||||
}).filter(({ lnglat }) => hasPinnedVehicleDistrictFilter() || !bounds || (lnglat[0] >= bounds.west && lnglat[0] <= bounds.east && lnglat[1] >= bounds.south && lnglat[1] <= bounds.north))
|
||||
.map(({ vehicle, lnglat }) => ({
|
||||
id: `vehicle-point-${vehicle.vin}`, kind: 'vehiclePoint', level: 'vehicle',
|
||||
nameZh: vehicle.plateNumber || vehicle.vin, nameEn: vehicle.plateNumber || vehicle.vin,
|
||||
@@ -1171,6 +1359,42 @@ function vehicleStatusLabel(vehicle) {
|
||||
return status;
|
||||
}
|
||||
|
||||
function navigationCoordinates(mode, entity) {
|
||||
const longitude = Number(entity?.longitude);
|
||||
const latitude = Number(entity?.latitude);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return null;
|
||||
// Vehicle telemetry is WGS-84 and is converted before rendering on AMap.
|
||||
// Station assets already use AMap (GCJ-02) coordinates.
|
||||
return mode === 'vehicle' ? wgs84ToGcj02(longitude, latitude) : [longitude, latitude];
|
||||
}
|
||||
|
||||
function navigationUrlForEntity(mode, entity) {
|
||||
const coordinates = navigationCoordinates(mode, entity);
|
||||
if (!coordinates?.every(Number.isFinite)) return null;
|
||||
const name = mode === 'station'
|
||||
? entity?.name || entity?.shortName || i18n[currentLang].detailStation
|
||||
: entity?.plateNumber || entity?.vin || i18n[currentLang].detailVehicle;
|
||||
const [longitude, latitude] = coordinates.map(value => Number(value).toFixed(6));
|
||||
return `https://uri.amap.com/navigation?to=${longitude},${latitude},${encodeURIComponent(name)}&mode=car&coordinate=gaode&callnative=1`;
|
||||
}
|
||||
|
||||
function updateDetailNavigation(mode, entity) {
|
||||
const button = document.getElementById('detailNavigateBtn');
|
||||
if (!button) return;
|
||||
const url = navigationUrlForEntity(mode, entity);
|
||||
const label = i18n[currentLang].detailNavigate;
|
||||
button.hidden = !url;
|
||||
button.disabled = !url;
|
||||
button.dataset.navigationUrl = url || '';
|
||||
button.setAttribute('aria-label', label);
|
||||
button.title = label;
|
||||
}
|
||||
|
||||
function navigateToSelectedEntity() {
|
||||
const url = selectedEntity && navigationUrlForEntity(selectedEntity.mode, selectedEntity.entity);
|
||||
if (url) window.location.assign(url);
|
||||
}
|
||||
|
||||
function showEntityDetails(mode, entity) {
|
||||
if (!entity) return;
|
||||
const dict = i18n[currentLang];
|
||||
@@ -1193,14 +1417,17 @@ function showEntityDetails(mode, entity) {
|
||||
title.textContent = entity.name || entity.shortName || dict.detailStation;
|
||||
subtitle.textContent = entity.shortName && entity.shortName !== entity.name ? entity.shortName : stationAdministrativePath(entity);
|
||||
const admin = [stationAdministrativePath(entity), stationDistrictName(entity)].filter(Boolean).filter((value, index, items) => index === 0 || !items[0].includes(value)).join(' · ');
|
||||
grid.innerHTML = [
|
||||
const publicFields = [
|
||||
detailField(dict.detailStationType, entity.cooperative ? dict.stationCooperative : dict.stationExternal),
|
||||
detailField(dict.detailAdmin, detailValue(admin)),
|
||||
detailField(dict.detailAddress, detailValue(entity.address), true),
|
||||
detailField(dict.detailCoordinate, `${formatNumber(entity.longitude, 6)}, ${formatNumber(entity.latitude, 6)}`, true, true)
|
||||
];
|
||||
const operationalFields = operationalAccessGranted() ? [
|
||||
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),
|
||||
detailField(dict.detailCoordinate, `${formatNumber(entity.longitude, 6)}, ${formatNumber(entity.latitude, 6)}`, true, true)
|
||||
].join('');
|
||||
] : [];
|
||||
grid.innerHTML = [...publicFields, ...operationalFields].join('');
|
||||
} else {
|
||||
title.textContent = entity.plateNumber || entity.vin || dict.detailVehicle;
|
||||
subtitle.textContent = entity.vin || '';
|
||||
@@ -1219,6 +1446,7 @@ function showEntityDetails(mode, entity) {
|
||||
}
|
||||
expand.hidden = false;
|
||||
expand.textContent = dict.detailMore;
|
||||
updateDetailNavigation(mode, entity);
|
||||
card.hidden = false;
|
||||
renderAMapMarkers();
|
||||
renderRankingList(currentRankType);
|
||||
@@ -1385,17 +1613,27 @@ 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;
|
||||
document.getElementById('rankSecondaryTab').textContent = currentMode === 'vehicle'
|
||||
? dict.rankByDist
|
||||
: operationalAccessGranted() ? dict.rankByHydrogen : dict.authHydrogenRequired;
|
||||
document.querySelectorAll('.gtab').forEach(button => button.classList.toggle('active', button.id === (currentRankType === 'primary' ? 'rankPrimaryTab' : 'rankSecondaryTab')));
|
||||
}
|
||||
|
||||
function switchRankTab(type) {
|
||||
if (currentMode === 'station' && type === 'secondary' && !operationalAccessGranted()) {
|
||||
openAccessDialog('station', i18n[currentLang].authHydrogenRequired);
|
||||
return;
|
||||
}
|
||||
currentRankType = type;
|
||||
updateRankingControls();
|
||||
renderRankingList(type);
|
||||
}
|
||||
|
||||
function switchMode(mode) {
|
||||
function switchMode(mode, { skipAuthCheck = false } = {}) {
|
||||
if (mode === 'vehicle' && !skipAuthCheck && !operationalAccessGranted()) {
|
||||
openAccessDialog('vehicle', i18n[currentLang].authRequired);
|
||||
return;
|
||||
}
|
||||
currentMode = mode;
|
||||
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
|
||||
closeExploreSuggestions();
|
||||
@@ -1409,6 +1647,7 @@ function switchMode(mode) {
|
||||
updateLocateButton(userLocation ? 'active' : 'idle');
|
||||
updateDashboardUI();
|
||||
if (mode === 'vehicle') refreshVehicleRegionNodes(dashboard);
|
||||
else refreshStationViewport();
|
||||
}
|
||||
|
||||
function setLanguage(lang) {
|
||||
@@ -1420,6 +1659,9 @@ function setLanguage(lang) {
|
||||
document.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n'); if (dict[key]) element.textContent = dict[key];
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
||||
const key = element.getAttribute('data-i18n-placeholder'); if (dict[key]) element.placeholder = dict[key];
|
||||
});
|
||||
const brandLogo = document.getElementById('brandLogo');
|
||||
const brandLogoEnglish = document.getElementById('brandLogoEnglish');
|
||||
if (brandLogo) {
|
||||
@@ -1430,6 +1672,7 @@ function setLanguage(lang) {
|
||||
}
|
||||
void syncFilterControls(dashboard);
|
||||
updateLocateButton(userLocation ? 'active' : 'idle');
|
||||
updateAccessUI();
|
||||
updateDashboardUI();
|
||||
if (selectedEntity?.entity) showEntityDetails(selectedEntity.mode, selectedEntity.entity);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user