refactor(map): remove access gate

This commit is contained in:
lingniu
2026-08-11 19:24:32 +08:00
parent d1ba129e99
commit 357dd490e9
9 changed files with 140 additions and 580 deletions
+42 -178
View File
@@ -23,14 +23,11 @@ const i18n = {
detailVehicle: '车辆', detailStation: '加氢站', detailPlate: '车牌', detailVin: 'VIN', detailStatus: '车辆状态', detailActive: '今日上线',
detailSpeed: '当前速度', detailDailyMileage: '今日里程', detailTotalMileage: '累计里程', detailLocation: '实时位置', detailCoordinate: '坐标',
detailStationType: '站点类型', detailAdmin: '行政区域', detailAddress: '详细地址', detailMonthlyHydrogen: '本月加氢量', detailTotalHydrogen: '累计加氢量', detailNavigate: '导航',
navigationLaunchingTitle: '正在打开高德地图', navigationLaunchingDescription: '正在准备导航路线,请稍候',
valueYes: '是', valueNo: '否', valueUnavailable: '暂无数据', locatePermissionHint: '无法获取位置,请在浏览器中允许位置权限后重试',
locateUnavailableHint: '暂时无法获取设备位置,请稍后重试', locateTimeoutHint: '定位超时,请到开阔区域后重试',
searchUniversal: '搜索位置、站点、车牌或 VIN', searchLocations: '位置', searchEntities: '业务对象', searchEmpty: '没有匹配的地点或对象',
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息',
publicStationTitle: '加氢站网络', publicDataStatus: '公共站点目录 · 位置、地址与合作状态', publicDataSource: '公共站点网络',
authEntry: '运营授权', authGranted: '已授权', authTitle: '进入运营数据', authDescription: '车辆全量信息与加氢量仅向获得授权的用户展示。',
authCodeLabel: '访问码', authCodePlaceholder: '输入访问码', authSubmit: '验证并进入', authCancel: '暂不授权', authRequired: '运营数据需要授权访问', authExpiry: '本次授权有效期为 30 分钟,到期后将自动恢复为公开站点视图。',
authDenied: '访问码无效,请重试', authUnavailable: '授权服务暂不可用,请稍后重试', authHydrogenRequired: '授权后查看加氢量', publicHydrogenHidden: '运营数据受保护', authLogout: '退出授权'
suggestionLocation: '地点', suggestionVehicle: '车辆', suggestionStation: '加氢站', detailMore: '展开全部信息', detailLess: '收起详细信息'
},
en: {
vehicleModeTitle: 'Vehicle Network', stationModeTitle: 'H₂ Station Network',
@@ -54,14 +51,11 @@ const i18n = {
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', 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.',
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',
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'
suggestionLocation: 'Location', suggestionVehicle: 'Vehicle', suggestionStation: 'H₂ station', detailMore: 'Show all details', detailLess: 'Collapse details'
}
};
@@ -116,8 +110,8 @@ let vehicleExploreCatalog = { ready: false, loading: null, options: [], province
let exploreSuggestionItems = [];
let activeExploreSuggestionIndex = -1;
let detailExpanded = false;
let navigationLaunchTimer = null;
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(['北京', '天津', '上海', '重庆']);
@@ -125,66 +119,19 @@ const MUNICIPALITIES = new Set(['北京', '天津', '上海', '重庆']);
document.addEventListener('DOMContentLoaded', () => {
initClock();
initAMapInstance();
initializeDataAccess();
loadDashboard();
refreshTimer = window.setInterval(loadDashboard, 15000);
});
document.addEventListener('pointerdown', event => {
if (!event.target?.closest?.('.map-explore-toolbar')) closeExploreSuggestions();
});
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() {
async function loadDashboard() {
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);
@@ -193,7 +140,7 @@ async function loadOperationalDashboard() {
else refreshStationViewport();
setDataState('ready');
} catch (error) {
console.error('operational dashboard refresh failed', error);
console.error('dashboard refresh failed', error);
setDataState('error');
}
}
@@ -205,97 +152,6 @@ 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();
}
@@ -864,9 +720,7 @@ 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' }
: operationalAccessGranted()
? { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' }
: { label: dict.publicHydrogenHidden, value: null, unit: '' };
: { label: dict.kpiMonthlyHydrogen, value: summary.monthlyHydrogenKg, unit: 'kg' };
document.getElementById('kpiActivityLabel').textContent = activity.label;
document.getElementById('kpiDailyDist').innerHTML = activity.value == null
? `${activity.unit ? ` <small>${activity.unit}</small>` : ''}`
@@ -884,17 +738,14 @@ 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}座外部站点${accessHint} · ${asOf}`
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations${accessHint} · ${asOf}`;
? `开放平台已同步 · ${summary.totalStations}座加氢站 · ${cooperative}座合作站点 · ${external}座外部站点 · ${asOf}`
: `Open platform synced · ${summary.totalStations} H₂ stations · ${cooperative} partner stations · ${external} external stations · ${asOf}`;
}
const regionText = regionSummary.loading ? regionLoadingText(regionSummary.level, lang) : regionSummaryText(regionSummary, lang);
return lang === 'zh'
? `开放平台已同步 · ${summary.totalVehicles}授权车辆 · ${regionText} · ${asOf}`
: `Open platform synced · ${summary.totalVehicles} authorized vehicles · ${regionText} · ${asOf}`;
? `开放平台已同步 · ${summary.totalVehicles}辆车辆 · ${regionText} · ${asOf}`
: `Open platform synced · ${summary.totalVehicles} vehicles · ${regionText} · ${asOf}`;
}
function modeKpiLabels(dict = i18n[currentLang], mode = currentMode) {
@@ -1390,9 +1241,31 @@ function updateDetailNavigation(mode, entity) {
button.title = label;
}
function resetNavigationLaunch() {
const overlay = document.getElementById('navigationLaunchOverlay');
const button = document.getElementById('detailNavigateBtn');
overlay?.classList.remove('is-visible');
window.setTimeout(() => { if (overlay) overlay.hidden = true; }, 180);
button?.classList.remove('is-launching');
if (button && button.dataset.navigationUrl) button.disabled = false;
}
function navigateToSelectedEntity() {
const url = selectedEntity && navigationUrlForEntity(selectedEntity.mode, selectedEntity.entity);
if (url) window.location.assign(url);
if (!url) return;
const overlay = document.getElementById('navigationLaunchOverlay');
const button = document.getElementById('detailNavigateBtn');
if (button?.classList.contains('is-launching')) return;
if (navigationLaunchTimer) window.clearTimeout(navigationLaunchTimer);
if (overlay) {
overlay.hidden = false;
window.requestAnimationFrame(() => overlay.classList.add('is-visible'));
}
button?.classList.add('is-launching');
if (button) button.disabled = true;
// Paint the acknowledgement before Safari hands control to the AMap URL scheme.
navigationLaunchTimer = window.setTimeout(() => window.location.assign(url), 140);
window.setTimeout(resetNavigationLaunch, 4000);
}
function showEntityDetails(mode, entity) {
@@ -1423,11 +1296,11 @@ 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 operationalFields = operationalAccessGranted() ? [
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, ...operationalFields].join('');
];
grid.innerHTML = [...publicFields, ...hydrogenFields].join('');
} else {
title.textContent = entity.plateNumber || entity.vin || dict.detailVehicle;
subtitle.textContent = entity.vin || '';
@@ -1615,25 +1488,17 @@ function updateRankingControls() {
document.getElementById('rankPrimaryTab').textContent = currentMode === 'vehicle' ? dict.rankByFleet : dict.rankByStations;
document.getElementById('rankSecondaryTab').textContent = currentMode === 'vehicle'
? dict.rankByDist
: operationalAccessGranted() ? dict.rankByHydrogen : dict.authHydrogenRequired;
: dict.rankByHydrogen;
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, { skipAuthCheck = false } = {}) {
if (mode === 'vehicle' && !skipAuthCheck && !operationalAccessGranted()) {
openAccessDialog('vehicle', i18n[currentLang].authRequired);
return;
}
function switchMode(mode) {
currentMode = mode;
filterState.province = ''; filterState.city = ''; filterState.district = ''; filterState.query = '';
closeExploreSuggestions();
@@ -1672,7 +1537,6 @@ function setLanguage(lang) {
}
void syncFilterControls(dashboard);
updateLocateButton(userLocation ? 'active' : 'idle');
updateAccessUI();
updateDashboardUI();
if (selectedEntity?.entity) showEntityDetails(selectedEntity.mode, selectedEntity.entity);
}