444 lines
17 KiB
JavaScript
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);
|
|
}
|