1145 行
39 KiB
JavaScript
1145 行
39 KiB
JavaScript
import {
|
||
calcScreenScale,
|
||
normalizeGranaryList,
|
||
formatGranaryParams,
|
||
aggregateApiGranaryData,
|
||
DESIGN_THEME,
|
||
DEFAULT_GRANARY_DATA,
|
||
buildQualityHtml,
|
||
buildQualityHudHtml,
|
||
mapVarietyToForecastType,
|
||
generateForecastSeries,
|
||
buildForecastTrendChartOption,
|
||
buildForecastCompareChartOption,
|
||
fetchGranaryForecast,
|
||
getGranaryQualityReport,
|
||
DEFAULT_AERIAL_MODE,
|
||
getAerialModeConfig,
|
||
getNextAerialMode,
|
||
CENTER_HOTSPOT_COORDS,
|
||
generateDepotAlerts
|
||
} from './grain_monitor_core.js';
|
||
|
||
// 全局状态
|
||
const STATE = {
|
||
activeId: 29, // 默认 31号仓房 (小麦优质样本)
|
||
filterType: 'all', // 'all' | 'warehouse' | 'oilTank'
|
||
qualityCategory: 'all', // 'all' | 'core' | 'physical' | 'safety'
|
||
isCenterChartOpen: false, // 居中大图表看板状态
|
||
centerChartTab: 'forecast', // 默认质量智能预测 'forecast' | 'macro'
|
||
autoPlay: false,
|
||
playTimer: null,
|
||
aerialMode: localStorage.getItem('depot_aerial_mode') || DEFAULT_AERIAL_MODE,
|
||
list: [],
|
||
envData: null,
|
||
customCoords: null, // 从后台同步的自定义3D热点坐标
|
||
summary: null,
|
||
charts: {
|
||
varietyPie: null,
|
||
granaryBar: null,
|
||
forecastTrend: null,
|
||
forecastCompare: null
|
||
},
|
||
forecastCache: {}
|
||
};
|
||
|
||
// 1. 电视大屏自适应缩放 (1920x1080)
|
||
function resizeScreen() {
|
||
const wrapper = document.getElementById('screen-wrapper');
|
||
if (!wrapper) return;
|
||
const winW = window.innerWidth;
|
||
const winH = window.innerHeight;
|
||
const scaleInfo = calcScreenScale(winW, winH, 1920, 1080);
|
||
|
||
wrapper.style.transform = `translate(${scaleInfo.offsetX}px, ${scaleInfo.offsetY}px) scale(${scaleInfo.scale})`;
|
||
|
||
if (STATE.charts.varietyPie) {
|
||
STATE.charts.varietyPie.resize();
|
||
}
|
||
if (STATE.charts.granaryBar) {
|
||
STATE.charts.granaryBar.resize();
|
||
}
|
||
if (STATE.charts.forecastTrend) {
|
||
STATE.charts.forecastTrend.resize();
|
||
}
|
||
if (STATE.charts.forecastCompare) {
|
||
STATE.charts.forecastCompare.resize();
|
||
}
|
||
}
|
||
|
||
// 2. 实时时钟
|
||
function updateClock() {
|
||
const clockEl = document.getElementById('header-clock');
|
||
const dateEl = document.getElementById('header-date');
|
||
if (!clockEl) return;
|
||
|
||
const now = new Date();
|
||
const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false });
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const date = String(now.getDate()).padStart(2, '0');
|
||
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||
const dayStr = weekDays[now.getDay()];
|
||
|
||
clockEl.textContent = timeStr;
|
||
if (dateEl) {
|
||
dateEl.textContent = `${year}-${month}-${date} ${dayStr}`;
|
||
}
|
||
}
|
||
|
||
// 3. 加载数据
|
||
async function fetchGranaryData() {
|
||
const endpoints = ['/api/index/granaryList', 'https://grain.gynhc.com/api/index/granaryList'];
|
||
for (const apiUrl of endpoints) {
|
||
try {
|
||
const res = await fetch(apiUrl, { cache: 'no-cache', signal: AbortSignal.timeout(2000) });
|
||
if (!res.ok) continue;
|
||
const json = await res.json();
|
||
if (json && json.data && Array.isArray(json.data.list) && json.data.list.length > 0) {
|
||
let list = json.data.list;
|
||
// 业务规范对齐:若线上外部API尚未同步最新规划 (缺少44号仓),自动执行“新增的是43,后面的仓库递增”
|
||
if (!list.some(item => item.name && item.name.includes('44号'))) {
|
||
list = list.map(item => {
|
||
if (item.name === '43号仓房' || (item.id === 46 && item.name.includes('43'))) {
|
||
return {
|
||
...item,
|
||
name: '44号仓房',
|
||
num: 44,
|
||
content: item.content ? { ...item.content, code: '44', warehouseType: '高大平房仓' } : item.content
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
|
||
if (!list.some(item => item.id === 51 || item.name === '43号仓房')) {
|
||
const default43 = DEFAULT_GRANARY_DATA?.data?.list?.find(i => i.id === 51);
|
||
if (default43) {
|
||
list.push(default43);
|
||
}
|
||
}
|
||
}
|
||
STATE.list = normalizeGranaryList({ ...json, data: { ...json.data, list } });
|
||
if (json.data && json.data.coords) {
|
||
STATE.customCoords = json.data.coords;
|
||
}
|
||
console.log(`✓ 成功从 [${apiUrl}] 在线加载`, STATE.list.length, '个仓房/油罐数据');
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
// 当前端点连接失败,自动尝试下一端点
|
||
}
|
||
}
|
||
|
||
console.warn('在线与本地 API 均不可用,使用内置官方完整数据集');
|
||
STATE.list = normalizeGranaryList(DEFAULT_GRANARY_DATA);
|
||
}
|
||
|
||
// 3.1 加载微气候环境工况
|
||
async function fetchEnvData() {
|
||
try {
|
||
const res = await fetch('/api/granary/env?site=center', { cache: 'no-cache' });
|
||
if (!res.ok) return;
|
||
const json = await res.json();
|
||
if (json && json.code === 0 && json.data) {
|
||
const d = json.data;
|
||
STATE.envData = d;
|
||
const inpTmI = document.getElementById('input-forecast-tm-i');
|
||
const inpTmO = document.getElementById('input-forecast-tm-o');
|
||
const inpHmI = document.getElementById('input-forecast-hm-i');
|
||
const inpHmO = document.getElementById('input-forecast-hm-o');
|
||
if (inpTmI && d.tm_i !== undefined) inpTmI.value = d.tm_i;
|
||
if (inpTmO && d.tm_o !== undefined) inpTmO.value = d.tm_o;
|
||
if (inpHmI && d.hm_i !== undefined) inpHmI.value = d.hm_i;
|
||
if (inpHmO && d.hm_o !== undefined) inpHmO.value = d.hm_o;
|
||
const pill = document.getElementById('forecast-trend-env-pill');
|
||
if (pill) {
|
||
pill.textContent = `工况: 仓内 ${d.tm_i}℃ / ${d.hm_i}% | 仓外 ${d.tm_o}℃ / ${d.hm_o}%`;
|
||
}
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
// 3.2 渲染全库储粮安全感知与预警走马灯
|
||
function renderDepotAlerts() {
|
||
const marqueeTrack = document.getElementById('alert-marquee-track');
|
||
if (!marqueeTrack) return;
|
||
const alerts = generateDepotAlerts(STATE.list, STATE.envData);
|
||
marqueeTrack.innerHTML = '';
|
||
const displayAlerts = alerts.length > 0 ? (alerts.length === 1 ? alerts : alerts.concat(alerts)) : [];
|
||
displayAlerts.forEach(alert => {
|
||
const pill = document.createElement('div');
|
||
pill.className = `alert-item-pill ${alert.level || 'normal'}`;
|
||
pill.innerHTML = `
|
||
<span class="pill-dot"></span>
|
||
<span class="pill-name">${alert.granaryName || '安全监控'}</span>
|
||
<span class="pill-text">${alert.text}</span>
|
||
`;
|
||
if (alert.granaryId) {
|
||
pill.style.cursor = 'pointer';
|
||
pill.title = '点击聚焦该仓房';
|
||
pill.onclick = (e) => {
|
||
e.stopPropagation();
|
||
selectGranary(alert.granaryId, true);
|
||
};
|
||
}
|
||
marqueeTrack.appendChild(pill);
|
||
});
|
||
}
|
||
|
||
// 4. 渲染顶部 KPIs
|
||
function renderKPIs() {
|
||
STATE.summary = aggregateApiGranaryData(STATE.list);
|
||
const s = STATE.summary;
|
||
|
||
document.getElementById('kpi-total-capacity').textContent = (s.totalCapacity / 10000).toFixed(2);
|
||
document.getElementById('kpi-total-stock').textContent = (s.totalStock / 10000).toFixed(2);
|
||
document.getElementById('kpi-utilization').textContent = `${s.utilizationRate}%`;
|
||
document.getElementById('kpi-warehouse-count').textContent = `${s.warehouseCount} 栋`;
|
||
document.getElementById('kpi-oil-count').textContent = `${s.oilTankCount} 座`;
|
||
}
|
||
|
||
// 5. 渲染 3D 鸟瞰图全景热点图钉 (20 个仓房/油罐全量覆盖,像素级对准与错层防重叠)
|
||
function renderHotspots() {
|
||
const container = document.getElementById('aerial-hotspots');
|
||
if (!container) return;
|
||
|
||
const hotspotCoords = STATE.customCoords ? { ...CENTER_HOTSPOT_COORDS, ...STATE.customCoords } : CENTER_HOTSPOT_COORDS;
|
||
|
||
container.innerHTML = '';
|
||
|
||
STATE.list.forEach(item => {
|
||
const coord = hotspotCoords[item.id];
|
||
if (!coord) return;
|
||
|
||
const isOil = item.isOilTank;
|
||
const isVisible = STATE.filterType === 'all' ||
|
||
(STATE.filterType === 'warehouse' && !isOil) ||
|
||
(STATE.filterType === 'oilTank' && isOil);
|
||
|
||
if (!isVisible) return;
|
||
|
||
const isActive = item.id === STATE.activeId;
|
||
const typeClass = isOil ? 'oil' : (item.grainName === '稻谷' ? 'paddy' : 'wheat');
|
||
const hasStem = !!coord.stemY;
|
||
const pin = document.createElement('div');
|
||
pin.className = `hotspot-pin ${typeClass} ${isActive ? 'active' : ''} ${hasStem ? 'has-stem' : ''}`;
|
||
pin.style.left = `${coord.x}%`;
|
||
pin.style.top = `${coord.y}%`;
|
||
|
||
const shortName = item.name.replace('号仓房', '号仓').replace('号油罐', '号罐');
|
||
|
||
let stemHtml = '';
|
||
if (hasStem) {
|
||
const stemX = coord.stemX !== undefined ? coord.stemX : coord.x;
|
||
const dxPx = (stemX - coord.x) * 13.6;
|
||
const dyPx = (coord.stemY - coord.y) * 7.59;
|
||
const lengthPx = Math.hypot(dxPx, dyPx);
|
||
const angleDeg = Math.atan2(-dxPx, dyPx) * (180 / Math.PI);
|
||
stemHtml = `
|
||
<div class="bubble-stem" style="height: ${lengthPx.toFixed(1)}px; transform: translateX(-50%) rotate(${angleDeg.toFixed(2)}deg);">
|
||
<span class="stem-dot"></span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
pin.innerHTML = `
|
||
<div class="bubble-inner">
|
||
<span class="bubble-name">${shortName}</span>
|
||
<span class="bubble-tag">${item.grainName}</span>
|
||
</div>
|
||
<div class="bubble-arrow"></div>
|
||
${stemHtml}
|
||
`;
|
||
|
||
pin.onclick = (e) => {
|
||
e.stopPropagation();
|
||
selectGranary(item.id, true);
|
||
};
|
||
|
||
container.appendChild(pin);
|
||
});
|
||
}
|
||
|
||
// 6. 渲染底部仓房/油罐快捷导航条 (纯净 20 仓罐双排点阵)
|
||
function renderMiniGrid() {
|
||
const container = document.getElementById('granary-mini-grid');
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
|
||
// 仓房/油罐按钮列表
|
||
STATE.list.forEach(item => {
|
||
const isOil = item.isOilTank;
|
||
const isVisible = STATE.filterType === 'all' ||
|
||
(STATE.filterType === 'warehouse' && !isOil) ||
|
||
(STATE.filterType === 'oilTank' && isOil);
|
||
if (!isVisible) return;
|
||
|
||
const btn = document.createElement('div');
|
||
const typeClass = isOil ? 'oil' : (item.grainName === '稻谷' ? 'paddy' : 'wheat');
|
||
const isActive = item.id === STATE.activeId;
|
||
btn.className = `mini-btn ${typeClass} ${isActive ? 'active' : ''}`;
|
||
const shortName = item.name.replace('号仓房', '号仓').replace('号油罐', '号罐');
|
||
btn.innerHTML = `<span class="btn-name">${shortName}</span><span class="btn-grain">${item.grainName}</span>`;
|
||
btn.title = `${item.name} (${item.grainName}) - 实存 ${item.storageNum} 吨`;
|
||
btn.onclick = () => selectGranary(item.id, true);
|
||
container.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// 7. 切换选中的仓房/油罐 (支持点击直接呼出居中大盘图表)
|
||
function selectGranary(id, openChart = false) {
|
||
STATE.activeId = id;
|
||
if (openChart) {
|
||
STATE.isCenterChartOpen = true;
|
||
STATE.centerChartTab = 'forecast';
|
||
const board = document.getElementById('center-chart-board');
|
||
if (board) {
|
||
board.style.display = 'flex';
|
||
switchCenterChartTab('forecast');
|
||
}
|
||
const closeBtn = document.getElementById('btn-close-center-chart');
|
||
if (closeBtn) {
|
||
closeBtn.onclick = () => toggleCenterChartBoard(STATE.centerChartTab, false);
|
||
}
|
||
bindCenterChartTabs();
|
||
} else if (STATE.isCenterChartOpen) {
|
||
if (STATE.centerChartTab === 'forecast') {
|
||
initOrUpdateCenterCharts();
|
||
}
|
||
}
|
||
renderMiniGrid();
|
||
renderHotspots();
|
||
renderDetailPanels();
|
||
}
|
||
|
||
// 居中大图表看板切换控制器
|
||
function toggleCenterChartBoard(targetTab = 'forecast', forceState) {
|
||
const reqTab = typeof targetTab === 'string' ? targetTab : (STATE.centerChartTab || 'forecast');
|
||
|
||
if (typeof forceState === 'boolean') {
|
||
STATE.isCenterChartOpen = forceState;
|
||
} else if (typeof targetTab === 'string') {
|
||
if (STATE.isCenterChartOpen && STATE.centerChartTab === targetTab) {
|
||
STATE.isCenterChartOpen = false;
|
||
} else {
|
||
STATE.isCenterChartOpen = true;
|
||
STATE.centerChartTab = targetTab;
|
||
}
|
||
} else {
|
||
STATE.isCenterChartOpen = !STATE.isCenterChartOpen;
|
||
}
|
||
|
||
const board = document.getElementById('center-chart-board');
|
||
if (board) {
|
||
board.style.display = STATE.isCenterChartOpen ? 'flex' : 'none';
|
||
if (STATE.isCenterChartOpen) {
|
||
switchCenterChartTab(STATE.centerChartTab);
|
||
}
|
||
}
|
||
|
||
// 同步绑定关闭按钮与模式切换 Tab
|
||
const closeBtn = document.getElementById('btn-close-center-chart');
|
||
if (closeBtn) {
|
||
closeBtn.onclick = () => toggleCenterChartBoard(STATE.centerChartTab, false);
|
||
}
|
||
bindCenterChartTabs();
|
||
renderMiniGrid();
|
||
}
|
||
|
||
// 绑定大盘顶部模式切换 Tab
|
||
function bindCenterChartTabs() {
|
||
const macroTab = document.getElementById('tab-chart-macro');
|
||
const forecastTab = document.getElementById('tab-chart-forecast');
|
||
if (macroTab) {
|
||
macroTab.onclick = () => switchCenterChartTab('macro');
|
||
}
|
||
if (forecastTab) {
|
||
forecastTab.onclick = () => switchCenterChartTab('forecast');
|
||
}
|
||
}
|
||
|
||
// 切换大盘模式 (宏观仓容品种 vs 粮食质量智能预测)
|
||
function switchCenterChartTab(tabName) {
|
||
STATE.centerChartTab = tabName;
|
||
const macroTabBtn = document.getElementById('tab-chart-macro');
|
||
const forecastTabBtn = document.getElementById('tab-chart-forecast');
|
||
const macroBody = document.getElementById('board-body-macro');
|
||
const forecastBody = document.getElementById('board-body-forecast');
|
||
const titleEl = document.getElementById('center-chart-header-title');
|
||
|
||
if (macroTabBtn && forecastTabBtn && macroBody && forecastBody) {
|
||
if (tabName === 'macro') {
|
||
macroTabBtn.classList.add('active');
|
||
forecastTabBtn.classList.remove('active');
|
||
macroBody.style.display = 'grid';
|
||
forecastBody.style.display = 'none';
|
||
if (titleEl) {
|
||
titleEl.textContent = '遵义直属库中心库区 · 全库储粮品种与仓容负荷综合大盘';
|
||
}
|
||
requestAnimationFrame(() => {
|
||
initOrUpdateVarietyPie();
|
||
initOrUpdateGranaryBar();
|
||
});
|
||
} else {
|
||
macroTabBtn.classList.remove('active');
|
||
forecastTabBtn.classList.add('active');
|
||
macroBody.style.display = 'none';
|
||
forecastBody.style.display = 'flex';
|
||
|
||
// 智能选取粮食仓房:若当前选中的是油罐,自动切换至第一个粮食平房仓/浅圆仓
|
||
let currentItem = STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
if (currentItem && !mapVarietyToForecastType(currentItem.grainName || currentItem.varieties?.name)) {
|
||
const grainWarehouse = STATE.list.find(i => mapVarietyToForecastType(i.grainName || i.varieties?.name));
|
||
if (grainWarehouse) {
|
||
STATE.activeId = grainWarehouse.id;
|
||
currentItem = grainWarehouse;
|
||
renderDetailPanels();
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(() => {
|
||
initOrUpdateForecastCharts(currentItem);
|
||
});
|
||
}
|
||
}
|
||
renderMiniGrid();
|
||
}
|
||
|
||
// 8. 渲染左侧面板:基础档案 + 仓容负荷 + 空间物理尺寸
|
||
function renderLeftPanel(item, params) {
|
||
if (!item || !params) return;
|
||
|
||
const b = params.basic;
|
||
const s = params.size;
|
||
|
||
document.getElementById('param-granary-name').textContent = item.name;
|
||
document.getElementById('param-granary-type').textContent = b.warehouseType;
|
||
document.getElementById('param-granary-variety').textContent = b.varieties;
|
||
document.getElementById('param-granary-nature').textContent = b.natureName;
|
||
document.getElementById('param-granary-addr').textContent = b.addr || b.productArea || '安徽/河南';
|
||
document.getElementById('param-granary-year').textContent = b.harvestYear || b.machiningYear || '2024';
|
||
document.getElementById('param-granary-enter').textContent = b.enterTime || '2024-08';
|
||
document.getElementById('param-granary-custodian').textContent = b.custodian || '肖胜利';
|
||
document.getElementById('param-granary-mgmt').textContent = b.managementForm || '直接管理';
|
||
|
||
// 仓容利用率
|
||
document.getElementById('param-capacity-val').textContent = b.capacity;
|
||
document.getElementById('param-storage-val').textContent = b.storageNum;
|
||
document.getElementById('param-util-percent').textContent = `${item.utilization}%`;
|
||
const fillBar = document.getElementById('param-util-bar');
|
||
if (fillBar) {
|
||
fillBar.style.width = `${Math.min(item.utilization, 100)}%`;
|
||
}
|
||
|
||
// 物理空间尺寸
|
||
const sizeGrid = document.getElementById('param-size-grid');
|
||
if (!sizeGrid) return;
|
||
|
||
if (item.isOilTank) {
|
||
sizeGrid.innerHTML = `
|
||
<div class="param-item">
|
||
<span class="param-label">油罐直径</span>
|
||
<span class="param-value highlight">${s.diameter}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">装油高度</span>
|
||
<span class="param-value highlight">${s.oilHeight}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">储油体积</span>
|
||
<span class="param-value">${s.oilVolume}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">储油方式</span>
|
||
<span class="param-value">立式储油罐密闭</span>
|
||
</div>
|
||
`;
|
||
} else {
|
||
sizeGrid.innerHTML = `
|
||
<div class="param-item">
|
||
<span class="param-label">仓房长度</span>
|
||
<span class="param-value highlight">${s.length}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">仓房跨度(宽)</span>
|
||
<span class="param-value highlight">${s.width}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">装粮线高</span>
|
||
<span class="param-value highlight">${s.height}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">粮堆体积</span>
|
||
<span class="param-value">${s.volume}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">扣除体积</span>
|
||
<span class="param-value">${s.decVolume}</span>
|
||
</div>
|
||
<div class="param-item">
|
||
<span class="param-label">储粮方式</span>
|
||
<span class="param-value">${b.storageMethod || '散装储粮'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 绑定右侧全量条形卡片分类 Tab 切换事件
|
||
function bindQualityTabs(container, report) {
|
||
if (!container || !report) return;
|
||
container.querySelectorAll('.quality-tab-btn').forEach(btn => {
|
||
btn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
const cat = btn.dataset.cat;
|
||
STATE.qualityCategory = cat;
|
||
container.innerHTML = buildQualityHudHtml(report, cat);
|
||
bindQualityTabs(container, report);
|
||
};
|
||
});
|
||
}
|
||
|
||
// 9. 渲染右侧面板:方案A 智能质检与品质全参数孪生舱 (100%全高长条卡片)
|
||
function renderRightPanel(item, params) {
|
||
if (!item || !params) return;
|
||
|
||
const qualityGrid = document.getElementById('param-quality-grid');
|
||
const qualityTitle = document.getElementById('param-quality-title');
|
||
const qualityExtra = qualityTitle ? qualityTitle.closest('.panel-title-bar')?.querySelector('.panel-extra') : null;
|
||
if (!qualityGrid) return;
|
||
|
||
const report = params.qualityReport;
|
||
if (!report) return;
|
||
|
||
// 标题与总体状态指示徽章 (短小精悍,杜绝截断)
|
||
if (qualityTitle) {
|
||
let statusBadge = '';
|
||
if (report.summary.dangerCount > 0) {
|
||
statusBadge = `<span class="quality-badge status-danger" style="margin-left: 5px;">⚠️ ${report.summary.dangerCount}项超标</span>`;
|
||
} else if (report.summary.warnCount > 0) {
|
||
statusBadge = `<span class="quality-badge status-warn" style="margin-left: 5px;">⚠️ ${report.summary.warnCount}项预警</span>`;
|
||
} else {
|
||
statusBadge = `<span class="quality-badge status-normal" style="margin-left: 5px;">✓ 健康达标</span>`;
|
||
}
|
||
qualityTitle.innerHTML = `<span>${report.variety}品质全参数舱</span>${statusBadge}`;
|
||
}
|
||
|
||
// 标题栏右侧显示批次阶段与检验日期 (精炼格式)
|
||
if (qualityExtra) {
|
||
const stageShort = (report.inspectStage || '').split('/')[0].trim();
|
||
qualityExtra.innerHTML = `<span style="font-size: 9px; color: var(--text-sub); white-space: nowrap;">${stageShort} · ${report.inspectDate}</span>`;
|
||
}
|
||
|
||
// 渲染方案A专属品质全参数舱 (全量水平条形卡片 + 分类Tab)
|
||
const curCat = STATE.qualityCategory || 'all';
|
||
qualityGrid.innerHTML = buildQualityHudHtml(report, curCat);
|
||
bindQualityTabs(qualityGrid, report);
|
||
}
|
||
|
||
// 初始化与更新中央大图表看板 (包含宏观大盘与质量预测推演)
|
||
function initOrUpdateCenterCharts() {
|
||
bindCenterChartTabs();
|
||
if (STATE.centerChartTab === 'forecast') {
|
||
const currentItem = STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
initOrUpdateForecastCharts(currentItem);
|
||
} else {
|
||
initOrUpdateVarietyPie();
|
||
initOrUpdateGranaryBar();
|
||
}
|
||
}
|
||
|
||
// 渲染粮食质量智能预测趋势图与偏离对比图
|
||
async function initOrUpdateForecastCharts(granaryItem) {
|
||
let currentItem = granaryItem || STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
if (!currentItem) return;
|
||
|
||
let grainName = currentItem.grainName || currentItem.varieties?.name || '粮食';
|
||
let grainType = mapVarietyToForecastType(grainName);
|
||
|
||
// 智能对齐:若当前选中的是油罐,自动切换至第一个粮食仓房 (小麦/稻谷) 确保 100% 呈现预测数据
|
||
if (!grainType) {
|
||
const grainWarehouse = STATE.list.find(i => mapVarietyToForecastType(i.grainName || i.varieties?.name));
|
||
if (grainWarehouse) {
|
||
STATE.activeId = grainWarehouse.id;
|
||
currentItem = grainWarehouse;
|
||
grainName = currentItem.grainName || currentItem.varieties?.name || '粮食';
|
||
grainType = mapVarietyToForecastType(grainName);
|
||
}
|
||
}
|
||
|
||
const titleEl = document.getElementById('center-chart-header-title');
|
||
const trendTitleEl = document.getElementById('forecast-trend-title');
|
||
const compareTitleEl = document.getElementById('forecast-compare-title');
|
||
|
||
if (titleEl) {
|
||
titleEl.textContent = `遵义直属库中心库区 · ${currentItem.name} (${grainName}) 质量指标演变智能预测推演`;
|
||
}
|
||
|
||
const trendDom = document.getElementById('chart-forecast-trend');
|
||
const compareDom = document.getElementById('chart-forecast-compare');
|
||
if (!trendDom || !compareDom || typeof echarts === 'undefined') return;
|
||
|
||
if (!STATE.charts.forecastTrend) {
|
||
STATE.charts.forecastTrend = echarts.init(trendDom);
|
||
}
|
||
if (!STATE.charts.forecastCompare) {
|
||
STATE.charts.forecastCompare = echarts.init(compareDom);
|
||
}
|
||
|
||
// 仓内外温湿度环境工况初始化与同步
|
||
if (!STATE.forecastEnv) {
|
||
STATE.forecastEnv = {
|
||
tm_i: 18.5,
|
||
tm_o: 32.5,
|
||
hm_i: 72.4,
|
||
hm_o: 54.2,
|
||
month: 7,
|
||
day: 13
|
||
};
|
||
}
|
||
|
||
const inputTmI = document.getElementById('input-forecast-tm-i');
|
||
const inputHmI = document.getElementById('input-forecast-hm-i');
|
||
const inputTmO = document.getElementById('input-forecast-tm-o');
|
||
const inputHmO = document.getElementById('input-forecast-hm-o');
|
||
const inputMonth = document.getElementById('input-forecast-month');
|
||
const inputDay = document.getElementById('input-forecast-day');
|
||
const pillEl = document.getElementById('forecast-trend-env-pill');
|
||
|
||
if (inputTmI) inputTmI.value = STATE.forecastEnv.tm_i;
|
||
if (inputHmI) inputHmI.value = STATE.forecastEnv.hm_i;
|
||
if (inputTmO) inputTmO.value = STATE.forecastEnv.tm_o;
|
||
if (inputHmO) inputHmO.value = STATE.forecastEnv.hm_o;
|
||
if (inputMonth) inputMonth.value = STATE.forecastEnv.month;
|
||
if (inputDay) inputDay.value = STATE.forecastEnv.day;
|
||
|
||
if (pillEl) {
|
||
pillEl.textContent = `工况: 仓内 ${STATE.forecastEnv.tm_i}℃ / ${STATE.forecastEnv.hm_i}% · 仓外 ${STATE.forecastEnv.tm_o}℃ / ${STATE.forecastEnv.hm_o}%`;
|
||
}
|
||
|
||
setupForecastEnvControls();
|
||
|
||
const currentReport = getGranaryQualityReport(grainName, currentItem.rawContent || currentItem.content || {});
|
||
|
||
// 1. 0 毫秒即时秒出基础图表 (杜绝任何白屏与残亮点)
|
||
const baselineList = [
|
||
{
|
||
day: 7,
|
||
targets: grainType === 'wheat'
|
||
? { wheat_moisture: 11.85, wheat_GWAC: 200.6, wheat_DON: 136.0, wheat_ZEN: 7.08 }
|
||
: { paddy_moisture: 11.66, paddy_fatacid: 25.9, paddy_AFB1: 2.45 }
|
||
},
|
||
{
|
||
day: 15,
|
||
targets: grainType === 'wheat'
|
||
? { wheat_moisture: 11.81, wheat_GWAC: 200.0, wheat_DON: 137.2, wheat_ZEN: 7.18 }
|
||
: { paddy_moisture: 11.62, paddy_fatacid: 26.4, paddy_AFB1: 2.52 }
|
||
},
|
||
{
|
||
day: 30,
|
||
targets: grainType === 'wheat'
|
||
? { wheat_moisture: 11.72, wheat_GWAC: 198.8, wheat_DON: 139.5, wheat_ZEN: 7.36 }
|
||
: { paddy_moisture: 11.55, paddy_fatacid: 27.3, paddy_AFB1: 2.64 }
|
||
},
|
||
{
|
||
day: 60,
|
||
targets: grainType === 'wheat'
|
||
? { wheat_moisture: 11.54, wheat_GWAC: 196.4, wheat_DON: 144.0, wheat_ZEN: 7.72 }
|
||
: { paddy_moisture: 11.40, paddy_fatacid: 29.1, paddy_AFB1: 2.88 }
|
||
}
|
||
];
|
||
|
||
let forecastList = STATE.forecastCache[currentItem.id];
|
||
if (!forecastList || forecastList.length === 0) {
|
||
const initialSeries = generateForecastSeries(grainType, currentReport, baselineList, STATE.forecastEnv);
|
||
const initialTrendOption = buildForecastTrendChartOption(initialSeries);
|
||
if (initialTrendOption) {
|
||
STATE.charts.forecastTrend.setOption(initialTrendOption, true);
|
||
}
|
||
const initialFuture30 = initialSeries.metrics.map(m => ({
|
||
key: m.key,
|
||
name: m.name,
|
||
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
|
||
}));
|
||
const initialCompareOption = buildForecastCompareChartOption(grainType, currentReport?.items || [], initialFuture30);
|
||
if (initialCompareOption) {
|
||
STATE.charts.forecastCompare.setOption(initialCompareOption, true);
|
||
}
|
||
STATE.charts.forecastTrend.resize();
|
||
STATE.charts.forecastCompare.resize();
|
||
|
||
// 并发快速拉取算法数据 (携带仓内外温湿度与时间参数)
|
||
try {
|
||
forecastList = await fetchGranaryForecast(currentItem, [7, 15, 30, 60], STATE.forecastEnv);
|
||
} catch {
|
||
forecastList = baselineList;
|
||
}
|
||
if (!forecastList || forecastList.length === 0) {
|
||
forecastList = baselineList;
|
||
}
|
||
STATE.forecastCache[currentItem.id] = forecastList;
|
||
}
|
||
|
||
// 2. 真实数据回传后平滑更新 (显式使用 notMerge=true 并携带环境工况)
|
||
const series = generateForecastSeries(grainType, currentReport, forecastList, STATE.forecastEnv);
|
||
|
||
if (trendTitleEl) {
|
||
trendTitleEl.textContent = `${currentItem.name} (${grainName}) 质量指标多周期演变推演 (当前实测与未来 7~60天)`;
|
||
}
|
||
const trendOption = buildForecastTrendChartOption(series);
|
||
if (trendOption) {
|
||
STATE.charts.forecastTrend.setOption(trendOption, true);
|
||
}
|
||
|
||
const future30Targets = series.metrics.map(m => ({
|
||
key: m.key,
|
||
name: m.name,
|
||
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
|
||
}));
|
||
|
||
if (compareTitleEl) {
|
||
compareTitleEl.textContent = `${currentItem.name} 现状实测 vs 未来30天预测终值偏离评估`;
|
||
}
|
||
const compareOption = buildForecastCompareChartOption(grainType, currentReport?.items || [], future30Targets);
|
||
if (compareOption) {
|
||
STATE.charts.forecastCompare.setOption(compareOption, true);
|
||
}
|
||
|
||
requestAnimationFrame(() => {
|
||
STATE.charts.forecastTrend?.resize();
|
||
STATE.charts.forecastCompare?.resize();
|
||
});
|
||
}
|
||
|
||
// 算法推演工况与温湿度输入交互控制器
|
||
function setupForecastEnvControls() {
|
||
const btnRecalc = document.getElementById('btn-forecast-recalc');
|
||
const btnReset = document.getElementById('btn-forecast-reset');
|
||
|
||
if (btnRecalc && !btnRecalc.dataset.bound) {
|
||
btnRecalc.dataset.bound = 'true';
|
||
btnRecalc.addEventListener('click', async () => {
|
||
const tm_i = parseFloat(document.getElementById('input-forecast-tm-i')?.value) || 18.5;
|
||
const hm_i = parseFloat(document.getElementById('input-forecast-hm-i')?.value) || 72.4;
|
||
const tm_o = parseFloat(document.getElementById('input-forecast-tm-o')?.value) || 32.5;
|
||
const hm_o = parseFloat(document.getElementById('input-forecast-hm-o')?.value) || 54.2;
|
||
const month = parseInt(document.getElementById('input-forecast-month')?.value, 10) || 7;
|
||
const day = parseInt(document.getElementById('input-forecast-day')?.value, 10) || 13;
|
||
|
||
STATE.forecastEnv = { tm_i, hm_i, tm_o, hm_o, month, day };
|
||
delete STATE.forecastCache[STATE.activeId];
|
||
|
||
const currentItem = STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
await initOrUpdateForecastCharts(currentItem);
|
||
});
|
||
}
|
||
|
||
if (btnReset && !btnReset.dataset.bound) {
|
||
btnReset.dataset.bound = 'true';
|
||
btnReset.addEventListener('click', async () => {
|
||
STATE.forecastEnv = {
|
||
tm_i: 18.5,
|
||
tm_o: 32.5,
|
||
hm_i: 72.4,
|
||
hm_o: 54.2,
|
||
month: 7,
|
||
day: 13
|
||
};
|
||
delete STATE.forecastCache[STATE.activeId];
|
||
|
||
const currentItem = STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
await initOrUpdateForecastCharts(currentItem);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 10. 渲染居中全库品种分布现代大环仪表盘 (Modern Doughnut Dashboard)
|
||
function initOrUpdateVarietyPie() {
|
||
const dom = document.getElementById('chart-variety-pie');
|
||
if (!dom || typeof echarts === 'undefined' || !STATE.summary) return;
|
||
|
||
if (!STATE.charts.varietyPie) {
|
||
STATE.charts.varietyPie = echarts.init(dom);
|
||
}
|
||
|
||
const dist = STATE.summary.distribution;
|
||
const pieData = Object.entries(dist).map(([name, val]) => {
|
||
const vTheme = DESIGN_THEME.getVarietyTheme(name);
|
||
return {
|
||
name,
|
||
value: Number((val / 10000).toFixed(2)),
|
||
itemStyle: {
|
||
color: vTheme.color
|
||
}
|
||
};
|
||
});
|
||
|
||
const totalStockWan = (STATE.summary.totalStock / 10000).toFixed(2);
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: '{b}: <strong>{c} 万吨</strong> ({d}%)',
|
||
backgroundColor: 'rgba(11, 22, 44, 0.94)',
|
||
borderColor: 'rgba(56, 189, 248, 0.4)',
|
||
borderWidth: 1,
|
||
padding: [8, 12],
|
||
textStyle: { color: '#f8fafc', fontSize: 12 }
|
||
},
|
||
title: {
|
||
text: totalStockWan,
|
||
subtext: '全库实储 (万吨)',
|
||
left: 'center',
|
||
top: '38%',
|
||
textStyle: {
|
||
color: '#f8fafc',
|
||
fontSize: 22,
|
||
fontWeight: 'bold',
|
||
fontFamily: 'DIN Alternate, JetBrains Mono, sans-serif'
|
||
},
|
||
subtextStyle: {
|
||
color: '#94a3b8',
|
||
fontSize: 11,
|
||
fontWeight: 'normal'
|
||
}
|
||
},
|
||
legend: {
|
||
bottom: '4%',
|
||
itemGap: 16,
|
||
textStyle: { color: '#cbd5e1', fontSize: 11 },
|
||
itemWidth: 12,
|
||
itemHeight: 12
|
||
},
|
||
series: [
|
||
{
|
||
name: '储粮品种结构',
|
||
type: 'pie',
|
||
radius: ['42%', '70%'],
|
||
center: ['50%', '46%'],
|
||
avoidLabelOverlap: false,
|
||
itemStyle: {
|
||
borderRadius: 6,
|
||
borderColor: '#060c20',
|
||
borderWidth: 3
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'outside',
|
||
color: '#e2e8f0',
|
||
fontSize: 11,
|
||
formatter: '{b}\n{d}%'
|
||
},
|
||
labelLine: {
|
||
show: true,
|
||
length: 10,
|
||
length2: 12,
|
||
lineStyle: {
|
||
color: 'rgba(56, 189, 248, 0.45)'
|
||
}
|
||
},
|
||
data: pieData
|
||
}
|
||
]
|
||
};
|
||
|
||
STATE.charts.varietyPie.setOption(option);
|
||
STATE.charts.varietyPie.resize();
|
||
}
|
||
|
||
// 渲染居中全库各仓储量与利用率负荷对比柱状图
|
||
function initOrUpdateGranaryBar() {
|
||
const dom = document.getElementById('chart-granary-bar');
|
||
if (!dom || typeof echarts === 'undefined' || !STATE.list || STATE.list.length === 0) return;
|
||
|
||
if (!STATE.charts.granaryBar) {
|
||
STATE.charts.granaryBar = echarts.init(dom);
|
||
}
|
||
|
||
const names = STATE.list.map(i => i.name.replace('号仓房', '#').replace('号油罐', 'T#'));
|
||
const storageData = STATE.list.map(i => i.storageNum);
|
||
const capacityData = STATE.list.map(i => i.capacity);
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' },
|
||
backgroundColor: 'rgba(11, 22, 44, 0.94)',
|
||
borderColor: 'rgba(56, 189, 248, 0.4)',
|
||
textStyle: { color: '#f8fafc', fontSize: 11 }
|
||
},
|
||
grid: {
|
||
top: '12%',
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '12%',
|
||
containLabel: true
|
||
},
|
||
legend: {
|
||
top: '0%',
|
||
right: '2%',
|
||
textStyle: { color: '#94a3b8', fontSize: 10.5 },
|
||
data: ['实存储量', '设计容量']
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: names,
|
||
axisLabel: {
|
||
color: '#94a3b8',
|
||
fontSize: 9.5,
|
||
interval: 0,
|
||
rotate: 30
|
||
},
|
||
axisLine: { lineStyle: { color: 'rgba(56, 189, 248, 0.2)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: '吨',
|
||
nameTextStyle: { color: '#64748b', fontSize: 10 },
|
||
axisLabel: { color: '#94a3b8', fontSize: 10 },
|
||
splitLine: { lineStyle: { color: 'rgba(56, 189, 248, 0.08)' } }
|
||
},
|
||
series: [
|
||
{
|
||
name: '实存储量',
|
||
type: 'bar',
|
||
barWidth: 8,
|
||
itemStyle: {
|
||
borderRadius: [3, 3, 0, 0],
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: '#38bdf8' },
|
||
{ offset: 1, color: '#0284c7' }
|
||
])
|
||
},
|
||
data: storageData
|
||
},
|
||
{
|
||
name: '设计容量',
|
||
type: 'bar',
|
||
barWidth: 8,
|
||
itemStyle: {
|
||
borderRadius: [3, 3, 0, 0],
|
||
color: 'rgba(255, 255, 255, 0.12)'
|
||
},
|
||
data: capacityData
|
||
}
|
||
]
|
||
};
|
||
|
||
STATE.charts.granaryBar.setOption(option);
|
||
STATE.charts.granaryBar.resize();
|
||
}
|
||
|
||
// 刷新全部详情
|
||
function renderDetailPanels() {
|
||
const currentItem = STATE.list.find(i => i.id === STATE.activeId) || STATE.list[0];
|
||
if (!currentItem) return;
|
||
|
||
const params = formatGranaryParams(currentItem);
|
||
renderLeftPanel(currentItem, params);
|
||
renderRightPanel(currentItem, params);
|
||
}
|
||
|
||
// 11. 自动轮播控制器 (每 5 秒切换下一仓)
|
||
function setupAutoPlay() {
|
||
const toggleBtn = document.getElementById('btn-autoplay');
|
||
const toggleText = document.getElementById('btn-autoplay-text');
|
||
if (!toggleBtn) return;
|
||
|
||
function start() {
|
||
stop();
|
||
STATE.playTimer = setInterval(() => {
|
||
if (!STATE.autoPlay || STATE.list.length === 0) return;
|
||
// 当居中大盘处于打开检视状态时,暂停自动轮播跳仓,保障人员专注于图表
|
||
if (STATE.isCenterChartOpen) return;
|
||
const curIdx = STATE.list.findIndex(i => i.id === STATE.activeId);
|
||
const nextIdx = (curIdx + 1) % STATE.list.length;
|
||
selectGranary(STATE.list[nextIdx].id);
|
||
}, 5000);
|
||
}
|
||
|
||
function stop() {
|
||
if (STATE.playTimer) {
|
||
clearInterval(STATE.playTimer);
|
||
STATE.playTimer = null;
|
||
}
|
||
}
|
||
|
||
toggleBtn.onclick = () => {
|
||
STATE.autoPlay = !STATE.autoPlay;
|
||
if (toggleText) {
|
||
toggleText.textContent = STATE.autoPlay ? '自动轮播: 开启' : '自动轮播: 关闭';
|
||
}
|
||
toggleBtn.classList.toggle('active', STATE.autoPlay);
|
||
if (STATE.autoPlay) {
|
||
start();
|
||
} else {
|
||
stop();
|
||
}
|
||
};
|
||
|
||
// 同步初始状态
|
||
if (toggleText) {
|
||
toggleText.textContent = STATE.autoPlay ? '自动轮播: 开启' : '自动轮播: 关闭';
|
||
}
|
||
toggleBtn.classList.toggle('active', STATE.autoPlay);
|
||
|
||
if (STATE.autoPlay) {
|
||
start();
|
||
}
|
||
}
|
||
|
||
// 12. 全屏与快捷键支持
|
||
function toggleFullscreen() {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen().catch(err => {
|
||
console.warn('全屏被阻止:', err);
|
||
});
|
||
} else {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 13. 鸟瞰图视觉模式切换 (默认 aerial_view2 实景日光)
|
||
function updateAerialViewUI(mode) {
|
||
const imgEl = document.getElementById('aerial-view-img');
|
||
const btnEl = document.getElementById('btn-aerial-switch');
|
||
const textEl = document.getElementById('btn-aerial-text');
|
||
const iconEl = document.getElementById('aerial-switch-icon');
|
||
const config = getAerialModeConfig(mode);
|
||
|
||
if (imgEl) {
|
||
if (imgEl.getAttribute('data-current-mode') !== config.key) {
|
||
imgEl.style.opacity = '0.6';
|
||
imgEl.src = config.path;
|
||
imgEl.setAttribute('data-current-mode', config.key);
|
||
setTimeout(() => {
|
||
imgEl.style.opacity = '1';
|
||
}, 50);
|
||
}
|
||
}
|
||
|
||
if (textEl) {
|
||
textEl.textContent = config.label;
|
||
}
|
||
|
||
if (btnEl) {
|
||
btnEl.classList.toggle('active', config.key === 'aerial_view2');
|
||
}
|
||
|
||
if (iconEl) {
|
||
if (config.key === 'aerial_view2') {
|
||
// 太阳 (实景日光)
|
||
iconEl.innerHTML = `
|
||
<circle cx="12" cy="12" r="5"></circle>
|
||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||
`;
|
||
} else {
|
||
// 月牙 (科技夜景)
|
||
iconEl.innerHTML = `
|
||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||
`;
|
||
}
|
||
}
|
||
}
|
||
|
||
function initAerialSwitch() {
|
||
const btnEl = document.getElementById('btn-aerial-switch');
|
||
if (!btnEl) return;
|
||
|
||
updateAerialViewUI(STATE.aerialMode);
|
||
|
||
btnEl.addEventListener('click', () => {
|
||
STATE.aerialMode = getNextAerialMode(STATE.aerialMode);
|
||
try {
|
||
localStorage.setItem('depot_aerial_mode', STATE.aerialMode);
|
||
} catch (_) {}
|
||
updateAerialViewUI(STATE.aerialMode);
|
||
});
|
||
}
|
||
|
||
// 初始化入口
|
||
window.addEventListener('DOMContentLoaded', async () => {
|
||
resizeScreen();
|
||
window.addEventListener('resize', resizeScreen);
|
||
|
||
updateClock();
|
||
setInterval(updateClock, 1000);
|
||
|
||
initAerialSwitch();
|
||
|
||
await fetchGranaryData();
|
||
await fetchEnvData();
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const paramId = parseInt(urlParams.get('id'));
|
||
if (paramId && STATE.list.some(i => i.id === paramId)) {
|
||
STATE.activeId = paramId;
|
||
}
|
||
if (urlParams.get('autoPlay') === 'true') {
|
||
STATE.autoPlay = true;
|
||
} else if (urlParams.get('autoPlay') === 'false') {
|
||
STATE.autoPlay = false;
|
||
}
|
||
|
||
renderKPIs();
|
||
renderHotspots();
|
||
renderMiniGrid();
|
||
renderDetailPanels();
|
||
renderDepotAlerts();
|
||
|
||
// 电视大屏 60 秒自动静默轮询同步最新台账与微气候
|
||
setInterval(async () => {
|
||
try {
|
||
await fetchGranaryData();
|
||
await fetchEnvData();
|
||
renderKPIs();
|
||
renderHotspots();
|
||
renderMiniGrid();
|
||
renderDetailPanels();
|
||
renderDepotAlerts();
|
||
} catch (_) {}
|
||
}, 60000);
|
||
|
||
setupAutoPlay();
|
||
|
||
const fsBtn = document.getElementById('btn-fullscreen');
|
||
if (fsBtn) fsBtn.addEventListener('click', toggleFullscreen);
|
||
|
||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
STATE.filterType = btn.getAttribute('data-filter');
|
||
renderHotspots();
|
||
renderMiniGrid();
|
||
});
|
||
});
|
||
|
||
window.addEventListener('keydown', (e) => {
|
||
const curIdx = STATE.list.findIndex(w => w.id === STATE.activeId);
|
||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||
const nextIdx = (curIdx - 1 + STATE.list.length) % STATE.list.length;
|
||
selectGranary(STATE.list[nextIdx].id);
|
||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||
const nextIdx = (curIdx + 1) % STATE.list.length;
|
||
selectGranary(STATE.list[nextIdx].id);
|
||
} else if (e.code === 'Space') {
|
||
toggleFullscreen();
|
||
} else if (e.key === 'v' || e.key === 'V') {
|
||
STATE.aerialMode = getNextAerialMode(STATE.aerialMode);
|
||
try {
|
||
localStorage.setItem('depot_aerial_mode', STATE.aerialMode);
|
||
} catch (_) {}
|
||
updateAerialViewUI(STATE.aerialMode);
|
||
}
|
||
});
|
||
});
|