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, CENTER_EXTRA_POSITIONS, resolveHotspotCoord, generateDepotAlerts, syncAlertMarqueeSpeed } from './grain_monitor_core.js?v=20261028_chart3'; // 全局状态 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) { const list = json.data.list; 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() { const endpoints = [ '/api/index/env?site=center', 'https://grain.gynhc.com/api/index/env?site=center', '/api/granary/env?site=center' ]; for (const url of endpoints) { try { const res = await fetch(url, { cache: 'no-cache', signal: AbortSignal.timeout(2000) }); if (!res.ok) continue; const json = await res.json(); if (json && (json.code === 1 || 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}%`; } return; } } catch (e) {} } } // 3.2 渲染全库储粮安全感知与预警走马灯 function renderDepotAlerts() { const marqueeTrack = document.getElementById('alert-marquee-track'); if (!marqueeTrack) return; const alerts = generateDepotAlerts(STATE.list, STATE.envData); const signature = alerts.map(a => `${a.id}|${a.text}`).join('\n'); if (marqueeTrack.dataset.alertSignature === signature && marqueeTrack.childElementCount > 0) return; marqueeTrack.dataset.alertSignature = signature; 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 = ` ${alert.granaryName || '安全监控'} ${alert.text} `; if (alert.granaryId) { pill.style.cursor = 'pointer'; pill.title = '点击聚焦该仓房'; pill.onclick = (e) => { e.stopPropagation(); selectGranary(alert.granaryId, true); }; } marqueeTrack.appendChild(pill); }); requestAnimationFrame(() => syncAlertMarqueeSpeed(marqueeTrack)); } // 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} 座`; const filterLabels = { all: `全部 (${STATE.list.length})`, warehouse: `平房仓群 (${s.warehouseCount})`, oilTank: `储油罐区 (${s.oilTankCount})` }; document.querySelectorAll('.filter-btn[data-filter]').forEach(button => { button.textContent = filterLabels[button.dataset.filter]; }); const totalCount = document.getElementById('depot-total-count'); if (totalCount) totalCount.textContent = `${STATE.list.length} 座/栋`; } // 5. 渲染 3D 鸟瞰图全景热点图钉 (20 个仓房/油罐全量覆盖,像素级对准与错层防重叠) function renderHotspots() { const container = document.getElementById('aerial-hotspots'); if (!container) return; container.innerHTML = ''; STATE.list.forEach(item => { const rawCoord = resolveHotspotCoord(item, STATE.customCoords, CENTER_HOTSPOT_COORDS); if (!rawCoord) return; const coord = { x: rawCoord.x !== undefined ? rawCoord.x : (rawCoord.left !== undefined ? rawCoord.left : 50), y: rawCoord.y !== undefined ? rawCoord.y : (rawCoord.top !== undefined ? rawCoord.top : 50), stemX: rawCoord.stemX, stemY: rawCoord.stemY }; 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.num}号${isOil ? '罐' : '仓'}`; 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 = `
`; } pin.innerHTML = `
${stemHtml} `; pin.onclick = (e) => { e.stopPropagation(); selectGranary(item.id, true); }; pin.querySelector('.bubble-name').textContent = shortName; pin.querySelector('.bubble-tag').textContent = item.grainName; container.appendChild(pin); }); // 未建台账的位置只显示待编号,不参与库存统计或质量预测。 if (STATE.filterType !== 'oilTank') { Object.entries(CENTER_EXTRA_POSITIONS).forEach(([position, coord]) => { if (STATE.list.some(item => item.screenPosition === position)) return; const pin = document.createElement('div'); pin.className = 'hotspot-pin wheat pending-warehouse'; pin.style.left = `${coord.x}%`; pin.style.top = `${coord.y}%`; pin.style.cursor = 'default'; pin.title = '请在后台粮仓管理添加仓库,选择对应的大屏建筑位置并填写仓号'; pin.innerHTML = '
待编号
'; 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.num}号${isOil ? '罐' : '仓'}`; btn.innerHTML = ''; btn.querySelector('.btn-name').textContent = shortName; btn.querySelector('.btn-grain').textContent = item.grainName; 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 = `
油罐直径 ${s.diameter}
装油高度 ${s.oilHeight}
储油体积 ${s.oilVolume}
储油方式 立式储油罐密闭
`; } else { sizeGrid.innerHTML = `
仓房长度 ${s.length}
仓房跨度(宽) ${s.width}
装粮线高 ${s.height}
粮堆体积 ${s.volume}
扣除体积 ${s.decVolume}
储粮方式 ${b.storageMethod || '散装储粮'}
`; } } // 绑定右侧全量条形卡片分类 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 = `⚠️ ${report.summary.dangerCount}项超标`; } else if (report.summary.warnCount > 0) { statusBadge = `⚠️ ${report.summary.warnCount}项预警`; } else { statusBadge = `✓ 健康达标`; } qualityTitle.innerHTML = `${report.variety}品质全参数舱${statusBadge}`; } // 标题栏右侧显示批次阶段与检验日期 (精炼格式) if (qualityExtra) { const stageShort = (report.inspectStage || '').split('/')[0].trim(); qualityExtra.innerHTML = `${stageShort} · ${report.inspectDate}`; } // 渲染方案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 || {}, currentItem.qualityTest); // 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, unit: m.unit, 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}) 多周期趋势`; } const trendOption = buildForecastTrendChartOption(series); if (trendOption) { STATE.charts.forecastTrend.setOption(trendOption, true); } const future30Targets = series.metrics.map(m => ({ key: m.key, name: m.name, unit: m.unit, predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1] })); if (compareTitleEl) { compareTitleEl.textContent = `${currentItem.name} 未来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}: {c} 万吨 ({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 = ` `; } else { // 月牙 (科技夜景) iconEl.innerHTML = ` `; } } } 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); } }); });