Files
vehicle-map-0803/app.js
T

444 lines
17 KiB
JavaScript

/**
* 羚牛氢能 - 车辆网络 (Bilingual & Restrained Apple Engine)
* Key: 1868920ac8ff6b6f88dbe9fa2609c183
* SecurityCode: 0b54a41143bec162788d01deba851340
*/
// ==========================================
// BILINGUAL TRANSLATION DICTIONARY
// ==========================================
const i18n = {
zh: {
vehicleModeTitle: '车辆网络',
stationModeTitle: '加氢站网络',
kpiTotalFleet: '车辆总数',
kpiOnlineFleet: '当前在线运行',
kpiDailyMileage: '今日运营里程',
unitVehicles: '辆',
unitStations: '座',
btnVehicle: '车辆',
btnStation: '加氢站',
themeDark: '深色',
themeLight: '浅色',
mapStatusText: '全域实时节点已同步 · 15个省份 · 42座加氢站',
legendPrimary: '核心区域 (>100)',
legendMid: '重点分布 (20-100)',
legendLow: '普通节点 (<20)',
btnResetView: '复位视角',
btn3dView: '3D/2D 视角',
btnNationalView: '全国视图',
mapFooterGis: '高质感地图引擎: 羚牛氢能GIS (AMap 3D Engine)',
mapFooterStatus: '系统状态: 稳定连接 (AES-256)',
hintNational: '视角: 全国运营态势',
hintProvince: '视角: {prov}专向统计',
panelStatusTitle: '车辆运营状态分布',
panelRankTitle: '重点区域运营 TOP 排名',
statusRunning: '运行中',
statusStopped: '静止中',
statusOffline: '离线',
rankByFleet: '按车辆',
rankByDist: '按里程',
onlineText: '在线',
fleetOverview: '📍 {prov}省运营概况',
fleetTotalLabel: '车辆总量',
onlineRunLabel: '在线运行',
unitTail: '台',
descLabel: '说明'
},
en: {
vehicleModeTitle: 'Vehicle Network',
stationModeTitle: 'H₂ Station Network',
kpiTotalFleet: 'Total Fleet',
kpiOnlineFleet: 'Active Online',
kpiDailyMileage: 'Today Mileage',
unitVehicles: 'units',
unitStations: 'stations',
btnVehicle: 'Fleet',
btnStation: 'H₂ Stations',
themeDark: 'Dark',
themeLight: 'Light',
mapStatusText: 'Real-time Nodes Synced · 15 Provinces · 42 H₂ Stations',
legendPrimary: 'Core (>100)',
legendMid: 'Key (20-100)',
legendLow: 'Standard (<20)',
btnResetView: 'Reset View',
btn3dView: '3D/2D View',
btnNationalView: 'National View',
mapFooterGis: 'Engine: Lingniu H₂ GIS (AMap 3D Engine)',
mapFooterStatus: 'Status: Secure Connected (AES-256)',
hintNational: 'View: National Overview',
hintProvince: 'View: {prov} Focus',
panelStatusTitle: 'Fleet Status Breakdown',
panelRankTitle: 'TOP Regional Operations',
statusRunning: 'Active',
statusStopped: 'Idle',
statusOffline: 'Offline',
rankByFleet: 'By Fleet',
rankByDist: 'By Mileage',
onlineText: 'Online',
fleetOverview: '📍 {prov} Province Overview',
fleetTotalLabel: 'Total Fleet',
onlineRunLabel: 'Active Fleet',
unitTail: 'units',
descLabel: 'Note'
}
};
// Province name English mapping
const provinceEnMap = {
'浙江': 'Zhejiang',
'广东': 'Guangdong',
'上海': 'Shanghai',
'江苏': 'Jiangsu',
'新疆': 'Xinjiang',
'湖北': 'Hubei',
'四川': 'Sichuan',
'陕西': 'Shaanxi',
'安徽': 'Anhui',
'重庆': 'Chongqing',
'山东': 'Shandong',
'天津': 'Tianjin',
'河南': 'Henan',
'北京': 'Beijing'
};
// ==========================================
// STATE MANAGEMENT (DEFAULT: LIGHT MODE)
// ==========================================
let map = null;
let markerList = [];
let is3DPitch = true;
let currentMode = 'vehicle'; // 'vehicle' | 'station'
let currentTheme = 'theme-white'; // DEFAULT: LIGHT MODE (纯白/浅色模式)
let currentLang = 'zh'; // 'zh' | 'en'
let selectedProvince = null;
let currentRankType = 'fleet';
// Province-Level Aggregates for Vehicle Operations
const vehicleGeoNodes = [
{ id: 'zj', name: '浙江', lnglat: [120.155070, 30.274085], count: 322, online: 286, dist: 168420, detail: '322台在途车辆 (在线率 88.8%)' },
{ id: 'gd', name: '广东', lnglat: [113.264385, 23.129112], count: 209, online: 175, dist: 124150, detail: '209台大湾区冷链运力' },
{ id: 'sh', name: '上海', lnglat: [121.473701, 31.230416], count: 54, online: 48, dist: 38200, detail: '54台港口短驳及城配集群' },
{ id: 'js', name: '江苏', lnglat: [118.796877, 32.060255], count: 35, online: 31, dist: 28900, detail: '35台49吨牵引车集群' },
{ id: 'xj', name: '新疆', lnglat: [87.617733, 43.792818], count: 28, online: 21, dist: 31200, detail: '28台重载矿用牵引车' },
{ id: 'hb', name: '湖北', lnglat: [114.305393, 30.593099], count: 27, online: 24, dist: 21800, detail: '27台双飞翼货车' },
{ id: 'sc', name: '四川', lnglat: [104.066541, 30.572269], count: 25, online: 22, dist: 24500, detail: '25台成渝零碳走廊车队' },
{ id: 'sx', name: '陕西', lnglat: [108.948024, 34.263161], count: 19, online: 16, dist: 14200, detail: '19台物流货车' },
{ id: 'ah', name: '安徽', lnglat: [117.283042, 31.861190], count: 19, online: 17, dist: 15600, detail: '19台长途牵引车' },
{ id: 'cq', name: '重庆', lnglat: [106.551556, 29.563009], count: 16, online: 14, dist: 12800, detail: '16台厢式货车' },
{ id: 'sd', name: '山东', lnglat: [117.000923, 36.675807], count: 90, online: 82, dist: 58300, detail: '90台青岛港重卡车队' },
{ id: 'tj', name: '天津', lnglat: [117.200983, 39.084158], count: 6, online: 5, dist: 4200, detail: '6台港口短驳车' },
{ id: 'hn', name: '河南', lnglat: [113.665412, 34.757975], count: 3, online: 3, dist: 2800, detail: '3台冷链物流车' },
{ id: 'bj', name: '北京', lnglat: [116.407526, 39.904030], count: 1, online: 1, dist: 980, detail: '1台平谷干线示范车' }
];
// Province-Level Station Data
const stationGeoNodes = [
{ id: 'gd-st', name: '广东', lnglat: [113.264385, 23.129112], count: '12 站', online: 11, detail: '南海撬装站、佛山示范站等 12座' },
{ id: 'sh-st', name: '上海', lnglat: [121.473701, 31.230416], count: '8 站', online: 8, detail: '嘉定70MPa加氢母站等 8座' },
{ id: 'bj-st', name: '北京', lnglat: [116.407526, 39.904030], count: '6 站', online: 6, detail: '大兴国际氢能枢纽站等 6座' },
{ id: 'js-st', name: '江苏', lnglat: [118.796877, 32.060255], count: '5 站', online: 5, detail: '苏州港加氢站等 5座' },
{ id: 'zj-st', name: '浙江', lnglat: [120.155070, 30.274085], count: '4 站', online: 4, detail: '嘉兴嘉善站等 4座' },
{ id: 'sd-st', name: '山东', lnglat: [117.000923, 36.675807], count: '3 站', online: 3, detail: '青岛港加氢站等 3座' },
{ id: 'sc-st', name: '四川', lnglat: [104.066541, 30.572269], count: '2 站', online: 2, detail: '成都皮口加注中心等 2座' },
{ id: 'hb-st', name: '湖北', lnglat: [114.305393, 30.593099], count: '2 站', online: 2, detail: '武汉东湖高新加氢站等 2座' }
];
// ==========================================
// INITIALIZATION
// ==========================================
document.addEventListener('DOMContentLoaded', () => {
initClock();
initAMapInstance();
renderRankingList(currentRankType);
});
function initAMapInstance() {
if (typeof AMap === 'undefined') return;
map = new AMap.Map('amapContainer', {
zoom: 4.8,
center: [108.948024, 34.263161],
viewMode: '3D',
pitch: 30,
mapStyle: getAMapThemeStyle(currentTheme),
showBuildingBlock: false,
showLabel: true
});
map.on('complete', () => {
renderAMapMarkers();
});
}
function getAMapThemeStyle(theme) {
return (theme === 'theme-white') ? 'amap://styles/light' : 'amap://styles/darkblue';
}
function getLocalizedProv(name) {
if (currentLang === 'en') {
return provinceEnMap[name] || name;
}
return name;
}
// Render Restrained Province Markers
function renderAMapMarkers() {
if (!map) return;
markerList.forEach(m => m.remove());
markerList = [];
const nodes = (currentMode === 'vehicle') ? vehicleGeoNodes : stationGeoNodes;
const cyanColor = getThemeColor('--accent-cyan', '#0284C7');
const cardBg = getThemeColor('--glass-bg', 'rgba(255, 255, 255, 0.85)');
const textMain = getThemeColor('--text-main', '#1D1D1F');
const cardBorder = getThemeColor('--glass-border', 'rgba(0, 0, 0, 0.08)');
const dict = i18n[currentLang];
nodes.forEach(node => {
const isSelected = (selectedProvince === node.name);
const dotColor = cyanColor;
const displayName = getLocalizedProv(node.name);
const markerContent = document.createElement('div');
markerContent.className = 'province-info-badge-wrap';
markerContent.innerHTML = `
<div class="province-info-card" style="
background: ${isSelected ? 'var(--pill-bg)' : cardBg};
border-color: ${isSelected ? dotColor : cardBorder};
">
<div class="p-head">
<span class="p-dot" style="background:${dotColor};"></span>
<span class="p-name">${displayName}</span>
<span class="p-total" style="color:${dotColor};">${node.count}</span>
</div>
<div class="p-sub">
${dict.onlineText} ${node.online}
</div>
</div>
`;
const marker = new AMap.Marker({
position: node.lnglat,
content: markerContent,
offset: new AMap.Pixel(-30, -12),
title: `${displayName}: ${node.count}`
});
const overviewTitle = dict.fleetOverview.replace('{prov}', displayName);
const infoWindow = new AMap.InfoWindow({
isCustom: true,
content: `
<div style="
background: ${cardBg};
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid ${cardBorder};
border-radius: 10px;
padding: 10px 14px;
color: ${textMain};
font-family: -apple-system, sans-serif;
min-width: 180px;
box-shadow: 0 12px 28px rgba(0,0,0,0.1);
">
<div style="font-weight:600; color:${textMain}; font-size:12px; margin-bottom:4px;">
${overviewTitle}
</div>
<div style="font-size:11px; color:var(--text-muted); line-height:1.4;">
<div>${dict.fleetTotalLabel}: <strong style="color:${textMain};">${node.count}</strong></div>
<div>${dict.onlineRunLabel}: <strong style="color:var(--accent-mint);">${node.online} ${dict.unitTail}</strong></div>
<div>${dict.descLabel}: ${node.detail}</div>
</div>
</div>
`,
offset: new AMap.Pixel(0, -32)
});
marker.on('mouseover', () => infoWindow.open(map, node.lnglat));
marker.on('mouseout', () => infoWindow.close());
marker.on('click', () => filterByProvince(node.name));
marker.setMap(map);
markerList.push(marker);
});
}
// ==========================================
// RIGHT PANEL INTERACTION & RANKING LIST
// ==========================================
function switchRankTab(type) {
currentRankType = type;
document.querySelectorAll('.gtab').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
renderRankingList(type);
}
function renderRankingList(type) {
const box = document.getElementById('rankingListBox');
if (!box) return;
box.innerHTML = '';
let list = [...vehicleGeoNodes];
if (type === 'fleet') {
list.sort((a, b) => b.count - a.count);
} else {
list.sort((a, b) => b.dist - a.dist);
}
const dict = i18n[currentLang];
list.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'rank-glass-row';
const topClass = idx === 0 ? 'r-top1' : (idx === 1 ? 'r-top2' : (idx === 2 ? 'r-top3' : ''));
const displayName = getLocalizedProv(item.name);
const valText = type === 'fleet' ? `${item.count} ${dict.unitVehicles}` : `${(item.dist / 1000).toFixed(1)}k km`;
div.innerHTML = `
<span class="r-badge ${topClass}">${idx + 1}</span>
<span class="r-name">${displayName}</span>
<span class="r-val">${valText}</span>
`;
div.onclick = () => filterByProvince(item.name);
box.appendChild(div);
});
}
function filterByProvince(provName) {
selectedProvince = provName;
const dict = i18n[currentLang];
const displayName = getLocalizedProv(provName);
document.getElementById('selectedRegionHint').textContent = dict.hintProvince.replace('{prov}', displayName);
renderAMapMarkers();
const match = vehicleGeoNodes.find(n => n.name === provName);
if (match && map) {
map.setZoomAndCenter(6.5, match.lnglat);
}
}
function clearProvinceFilter() {
selectedProvince = null;
const dict = i18n[currentLang];
document.getElementById('selectedRegionHint').textContent = dict.hintNational;
resetMapView();
renderAMapMarkers();
}
// ==========================================
// LANGUAGE SWITCHER (ZH / EN)
// ==========================================
function setLanguage(lang) {
currentLang = lang;
document.querySelectorAll('.lang-switcher-bw .segment-btn').forEach(b => b.classList.remove('active'));
if (lang === 'zh') document.querySelector('.lang-zh-btn')?.classList.add('active');
if (lang === 'en') document.querySelector('.lang-en-btn')?.classList.add('active');
const dict = i18n[lang];
// Translate static data-i18n attributes
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (dict[key]) {
el.textContent = dict[key];
}
});
// Dynamic titles & KPIs
if (currentMode === 'vehicle') {
document.getElementById('dashboardTitle').textContent = dict.vehicleModeTitle;
document.getElementById('kpiFleetTotal').innerHTML = `1,024 <small>${dict.unitVehicles}</small>`;
document.getElementById('kpiFleetOnline').innerHTML = `823 <small>${dict.unitVehicles} (80.4%)</small>`;
} else {
document.getElementById('dashboardTitle').textContent = dict.stationModeTitle;
document.getElementById('kpiFleetTotal').innerHTML = `42 <small>${dict.unitStations}</small>`;
document.getElementById('kpiFleetOnline').innerHTML = `39 <small>${dict.unitStations} (92.8%)</small>`;
}
// Update selected region hint
if (selectedProvince) {
const displayName = getLocalizedProv(selectedProvince);
document.getElementById('selectedRegionHint').textContent = dict.hintProvince.replace('{prov}', displayName);
} else {
document.getElementById('selectedRegionHint').textContent = dict.hintNational;
}
renderRankingList(currentRankType);
renderAMapMarkers();
}
// ==========================================
// MODE & BLACK/WHITE THEME SWITCHER
// ==========================================
function switchMode(mode) {
currentMode = mode;
const dict = i18n[currentLang];
const btnVehicle = document.getElementById('btnModeVehicle');
const btnStation = document.getElementById('btnModeStation');
if (mode === 'vehicle') {
btnVehicle.classList.add('active');
btnStation.classList.remove('active');
document.getElementById('dashboardTitle').textContent = dict.vehicleModeTitle;
document.getElementById('kpiFleetTotal').innerHTML = `1,024 <small>${dict.unitVehicles}</small>`;
document.getElementById('kpiFleetOnline').innerHTML = `823 <small>${dict.unitVehicles} (80.4%)</small>`;
} else {
btnStation.classList.add('active');
btnVehicle.classList.remove('active');
document.getElementById('dashboardTitle').textContent = dict.stationModeTitle;
document.getElementById('kpiFleetTotal').innerHTML = `42 <small>${dict.unitStations}</small>`;
document.getElementById('kpiFleetOnline').innerHTML = `39 <small>${dict.unitStations} (92.8%)</small>`;
}
renderAMapMarkers();
}
function setTheme(themeName) {
currentTheme = themeName;
document.body.className = themeName;
document.querySelectorAll('.bw-btn').forEach(b => b.classList.remove('active'));
if (themeName === 'theme-dark') document.querySelector('.theme-dark-btn')?.classList.add('active');
if (themeName === 'theme-white') document.querySelector('.theme-white-btn')?.classList.add('active');
if (map) {
map.setMapStyle(getAMapThemeStyle(themeName));
renderAMapMarkers();
}
}
function getThemeColor(varName, fallback) {
const val = getComputedStyle(document.body).getPropertyValue(varName).trim();
return val || fallback;
}
function resetMapView() {
if (map) {
map.setZoomAndCenter(4.8, [108.948024, 34.263161]);
map.setPitch(30);
}
}
function togglePitchView() {
if (!map) return;
is3DPitch = !is3DPitch;
map.setPitch(is3DPitch ? 30 : 0);
}
function initClock() {
const clockTime = document.getElementById('clockTime');
function update() {
const now = new Date();
const hrs = String(now.getHours()).padStart(2, '0');
const mins = String(now.getMinutes()).padStart(2, '0');
const secs = String(now.getSeconds()).padStart(2, '0');
if (clockTime) clockTime.textContent = `${hrs}:${mins}:${secs}`;
}
update();
setInterval(update, 1000);
}