中国建设人才信息网站官网,网站黑链检测,优秀国外设计网站app,怎么欣赏一个网站设计图76个工业组件库示例汇总
电网数据管理与智能分析组件
1. 组件概述
本组件旨在模拟一个城市配电网的运行状态#xff0c;重点关注数据管理、可视化以及基于模拟数据的智能分析#xff0c;特别是负载预测功能。用户可以通过界面交互式地探索电网拓扑、查看节点状态、控制时间…76个工业组件库示例汇总
电网数据管理与智能分析组件
1. 组件概述
本组件旨在模拟一个城市配电网的运行状态重点关注数据管理、可视化以及基于模拟数据的智能分析特别是负载预测功能。用户可以通过界面交互式地探索电网拓扑、查看节点状态、控制时间演进并观察系统生成的负载预测和相关告警。
设计风格遵循苹果科技工业美学力求界面清晰、交互流畅、信息直观。
2. 主要功能
实时概览指标: 顶部显示当前电网总负载 (MW)、未来24小时预测峰值负载 (MW) 以及一个概念性的电网稳定指数。电网拓扑可视化: 左侧区域使用简化的图形展示变电站和馈线的连接关系。节点和连接线的颜色会根据实时模拟的负载状态低、正常、高、过载、离线动态变化。时间演进控制: 用户可以播放/暂停模拟时间的流逝调整模拟速度1x, 5x, 10x, 30x或将时间重置到初始状态。负载预测图表: 右上侧使用 Chart.js 图表展示选中节点默认为总负载或首个馈线点击拓扑图节点切换的负载曲线包括过去几小时的历史负载和未来24小时的预测负载。节点详细数据: 点击左侧拓扑图中的节点变电站或馈线右侧中间面板会显示该节点的详细信息如ID、名称、类型、当前负载、电压、容量如有、状态和预测峰值。智能分析与告警: 右下侧面板根据当前的电网状态和预测结果自动生成告警信息如节点当前过载和预警信息如预测到未来几小时内可能发生过载以及基于稳定指数的建议。响应式布局: 界面适应不同宽度的浏览器窗口在中小型屏幕上会自动调整布局并控制整体高度防止内容过长。
3. 技术栈
HTML5CSS3 (Flexbox, Grid, CSS Variables, Media Queries)JavaScript (ES6)Chart.js (用于绘制图表)Day.js (用于日期和时间处理)chartjs-adapter-dayjs (Chart.js 的 Day.js 适配器)
4. 运行与使用
将 grid-data-smart-analysis 文件夹放置在 能源管理组件 目录下。在支持 HTML5 和 JavaScript 的浏览器中打开 index.html 文件。组件加载后模拟处于暂停状态显示初始电网拓扑和数据。点击左下角的播放按钮 (▶️) 开始模拟时间的演进观察拓扑图颜色、图表和告警信息的变化。使用暂停 (⏸️)、速度下拉框和重置按钮控制模拟进程。点击左侧拓扑图中的任意节点圆形代表馈线方形代表变电站来查看该节点的详细数据和负载预测曲线。
5. 模拟逻辑说明
电网拓扑: 在 script.js 中定义了一个包含节点变电站、馈线和连接关系的简化电网结构。节点位置使用百分比定义以便在不同尺寸下绘制。负载模式: 每个馈线节点预定义了一个24小时的基础负载曲线 (baseLoad) 和一个周末负载系数 (weekendMultiplier)。模拟器根据当前模拟时间小时和星期几来计算基础负载。负载计算: 节点的实际负载 基础负载 * (1 /- 随机波动%)。变电站负载: 简单设定为其所连接的所有下游节点馈线或其他变电站的负载之和。负载状态: 根据节点当前负载与其容量 (capacity) 的比例判定为低、正常、高或过载状态。电压模拟: 仅模拟小幅度的随机波动未与负载严格关联。负载预测: 高度简化。对于馈线基于其未来的基础负载模式进行预测并加入微小波动。对于变电站预测负载为其所连接馈线的预测负载之和。总负载/峰值预测: 当前总负载为所有馈线负载之和预测峰值为所有馈线预测负载在未来24小时内的最大总和。稳定指数: 基于当前过载和高负载节点的数量计算出的概念性分数。告警/预警: 基于当前节点是否过载以及预测负载是否会超过节点容量来生成。
6. 注意事项
这是一个高度简化的概念性模拟其电网拓扑、负载模型、电压模拟和特别是负载预测算法都与实际电力系统工程相去甚远。主要目的是演示一个集成化的电网数据监控与分析界面的设计思路、交互方式和数据可视化效果。所有数据均为程序生成不代表任何真实的电网运行数据。
效果展示 源码
index.html
!DOCTYPE html
html langzh-CN
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title电网数据管理与智能分析/titlelink relstylesheet hrefstyles.css!-- Script loading order changed --script srchttps://cdn.jsdelivr.net/npm/dayjs1/dayjs.min.js/script !-- 1. Day.js Core --script srchttps://cdn.jsdelivr.net/npm/chartjs-adapter-dayjs1/dist/chartjs-adapter-dayjs.bundle.min.js/script !-- 2. Day.js Adapter --script srchttps://cdn.jsdelivr.net/npm/chart.js/script !-- 3. Chart.js --
/head
bodydiv classcontainerheader classoverview-barh1城市配电网数据管理与智能分析/h1div classmetricsdiv classmetric-itemspan classlabel当前总负载/spanspan classvalue idcurrentTotalLoad-- MW/span/divdiv classmetric-itemspan classlabel预测峰值 (24h)/spanspan classvalue idpredictedPeakLoad-- MW/span/divdiv classmetric-itemspan classlabel电网稳定指数/spanspan classvalue idgridStabilityIndex--/spanspan classtooltip概念性指标越高越稳定/span/div/div/headermain classmain-contentsection classgrid-visualization-sectiondiv classtopology-containerh2电网拓扑与状态 (简化)/h2div classtopology-map idtopologyMap!-- Grid nodes and lines will be generated by JS --p正在加载电网拓扑.../p/divdiv classlegendspan负载状态:/spanspan classlegend-item low/span 低span classlegend-item normal/span 正常span classlegend-item high/span 高span classlegend-item overload/span 过载span classlegend-item offline/span 离线/div/divdiv classtime-control-panelh2时间控制/h2label forcurrentDateTime当前时间:/labelinput typedatetime-local idcurrentDateTime disabledbutton idplayPauseBtn title播放/暂停时间演进▶️/buttonlabel fortimeSpeed速度:/labelselect idtimeSpeedoption value11x/optionoption value55x/optionoption value1010x/optionoption value3030x/option/selectbutton idresetTimeBtn title重置时间重置/button/div/sectionsection classdata-analysis-sectiondiv classchart-container load-forecast-containerh2负载预测 (未来 24 小时)/h2canvas idloadForecastChart/canvas/divdiv classnode-data-panelh2节点数据: span idselectedNodeName未选择/span/h2div idnodeDetailsp请在左侧拓扑图中选择一个节点查看详细数据。/p!-- Details like Current Load, Voltage, Predicted Peak, Status --/div/divdiv classanalysis-alerts-panelh2智能分析与告警/h2ul idalertListli系统初始化完成等待数据.../li!-- Analysis results and alerts will be added by JS --/ul/div/section/mainfooter classstatus-barspan模拟时间: span idsimulationTime--/span/spanspan模拟状态: span idsimulationStatus已暂停/span/span/footer/divscript srcscript.js/script !-- 4. Your main script --
/body
/html styles.css
:root {--bg-color: #f5f5f7;--panel-bg-color: #ffffff;--border-color: #d2d2d7;--text-color-primary: #1d1d1f;--text-color-secondary: #6e6e73;--accent-blue: #007aff;--accent-green: #34c759;--accent-yellow: #ffcc00;--accent-orange: #ff9500;--accent-red: #ff3b30;--font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;--border-radius: 8px;--container-padding: 20px;--panel-padding: 15px;--header-height: 60px;--footer-height: 30px; /* Smaller footer *//* Grid status colors */--load-low-color: #a1dd70;--load-normal-color: var(--accent-green);--load-high-color: var(--accent-yellow);--load-overload-color: var(--accent-red);--load-offline-color: #a0a0a0;
}* {box-sizing: border-box;margin: 0;padding: 0;
}body {font-family: var(--font-family);background-color: var(--bg-color);color: var(--text-color-primary);line-height: 1.5;display: flex;justify-content: center;align-items: flex-start;min-height: 100vh;padding: 20px;
}.container {width: 100%;max-width: 1500px; /* Slightly wider for grid layout */background-color: var(--panel-bg-color);border-radius: var(--border-radius);border: 1px solid var(--border-color);box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);overflow: hidden;display: flex;flex-direction: column;
}/* Header / Overview Bar */
.overview-bar {display: flex;justify-content: space-between;align-items: center;padding: 0 var(--container-padding);height: var(--header-height);border-bottom: 1px solid var(--border-color);background-color: #ffffff;
}.overview-bar h1 {font-size: 1.2em;font-weight: 600;color: var(--text-color-primary);white-space: nowrap;overflow: hidden;text-overflow: ellipsis;margin-right: 20px;
}.metrics {display: flex;gap: 25px;flex-shrink: 0; /* Prevent metrics from shrinking */
}.metric-item {display: flex;flex-direction: column;align-items: flex-end;position: relative; /* For tooltip */
}.metric-item .label {font-size: 0.8em;color: var(--text-color-secondary);margin-bottom: 2px;
}.metric-item .value {font-size: 1.1em;font-weight: 600;color: var(--text-color-primary);
}.metric-item .tooltip {position: absolute;bottom: 100%; /* Position above the item */left: 50%;transform: translateX(-50%);background-color: rgba(0, 0, 0, 0.7);color: white;padding: 3px 6px;border-radius: 4px;font-size: 0.7em;white-space: nowrap;opacity: 0;visibility: hidden;transition: opacity 0.2s ease, visibility 0.2s ease;margin-bottom: 5px;pointer-events: none; /* Prevent tooltip from blocking clicks */
}.metric-item:hover .tooltip {opacity: 1;visibility: visible;
}/* Main Content Area */
.main-content {display: flex;flex: 1;padding: var(--container-padding);gap: var(--container-padding);min-height: 450px; /* Minimum height for layout */
}.grid-visualization-section {flex: 2; /* Left side takes less space */display: flex;flex-direction: column;gap: var(--container-padding);
}.data-analysis-section {flex: 3; /* Right side takes more space */display: flex;flex-direction: column;gap: var(--container-padding);
}/* Panels within sections */
.topology-container,
.time-control-panel,
.chart-container,
.node-data-panel,
.analysis-alerts-panel {background-color: var(--panel-bg-color);border-radius: var(--border-radius);padding: var(--panel-padding);box-shadow: 0 1px 3px rgba(0,0,0,0.04);display: flex;flex-direction: column; /* Default to column layout */
}.topology-container h2,
.time-control-panel h2,
.chart-container h2,
.node-data-panel h2,
.analysis-alerts-panel h2 {font-size: 0.95em;font-weight: 600;margin-bottom: 15px;color: var(--text-color-primary);flex-shrink: 0; /* Prevent title shrinking */
}/* Left Side Panels */
.topology-container {flex-grow: 1; /* Allow topology to take available space */min-height: 300px; /* Ensure space for map */
}.topology-map {flex-grow: 1;background-color: #e9e9eb;border-radius: 4px;position: relative; /* For positioning nodes/lines */overflow: auto; /* Allow scroll if content exceeds */display: flex; /* Center initial message */justify-content: center;align-items: center;color: var(--text-color-secondary);
}/* Simple placeholder styling for nodes/lines - JS will handle real elements */
.grid-node {position: absolute;width: 30px;height: 30px;border-radius: 50%;background-color: var(--accent-blue);border: 2px solid white;box-shadow: 0 1px 3px rgba(0,0,0,0.2);display: flex;justify-content: center;align-items: center;font-size: 0.7em;font-weight: bold;color: white;cursor: pointer;transition: transform 0.2s ease, background-color 0.3s ease;z-index: 2;
}
.grid-node:hover {transform: scale(1.1);
}
.grid-node.selected {border-color: var(--accent-orange);box-shadow: 0 0 8px var(--accent-orange);
}.grid-line {position: absolute;background-color: var(--text-color-secondary);height: 3px; /* Line thickness */transform-origin: left center;z-index: 1;transition: background-color 0.3s ease;
}/* Node status colors (applied via JS) */
.grid-node.low, .grid-line.low { background-color: var(--load-low-color); }
.grid-node.normal, .grid-line.normal { background-color: var(--load-normal-color); }
.grid-node.high, .grid-line.high { background-color: var(--load-high-color); }
.grid-node.overload, .grid-line.overload { background-color: var(--load-overload-color); }
.grid-node.offline, .grid-line.offline { background-color: var(--load-offline-color); }/* Legend Styling */
.legend {margin-top: 10px;font-size: 0.75em;display: flex;align-items: center;flex-wrap: wrap;gap: 5px 10px;color: var(--text-color-secondary);flex-shrink: 0;
}
.legend-item {display: inline-block;width: 12px;height: 12px;border-radius: 3px;margin-right: 3px;vertical-align: middle;
}
.legend-item.low { background-color: var(--load-low-color); }
.legend-item.normal { background-color: var(--load-normal-color); }
.legend-item.high { background-color: var(--load-high-color); }
.legend-item.overload { background-color: var(--load-overload-color); }
.legend-item.offline { background-color: var(--load-offline-color); }.time-control-panel {flex-shrink: 0; /* Prevent panel from shrinking */
}
.time-control-panel label {font-size: 0.85em;margin-right: 5px;color: var(--text-color-secondary);
}
.time-control-panel input[typedatetime-local],
.time-control-panel select {font-size: 0.85em;padding: 4px 6px;border: 1px solid var(--border-color);border-radius: 4px;margin-right: 10px;
}
.time-control-panel button {font-size: 1em;background: none;border: none;cursor: pointer;padding: 5px;margin: 0 5px;vertical-align: middle;
}
.time-control-panel button:hover {opacity: 0.7;
}/* Right Side Panels */
.load-forecast-container {flex-grow: 2; /* Chart takes more space */min-height: 250px;
}
.node-data-panel {flex-grow: 1;min-height: 100px;
}
.analysis-alerts-panel {flex-grow: 1;max-height: 180px; /* Limit height */overflow-y: auto;
}.chart-container canvas {max-width: 100%;flex-grow: 1; /* Allow canvas to fill container */
}#nodeDetails p {font-size: 0.9em;color: var(--text-color-secondary);
}
#nodeDetails strong {color: var(--text-color-primary);
}
#nodeDetails span {margin-left: 5px;
}/* Alerts List Styling (similar to previous component) */
#alertList {list-style: none;padding: 0;font-size: 0.85em;flex-grow: 1;overflow-y: auto; /* Scroll within the list */
}#alertList li {padding: 6px 10px;border-bottom: 1px solid #eee;display: flex;align-items: center;gap: 8px;
}#alertList li:last-child {border-bottom: none;
}/* Alert types styling */
.alert-info::before { content: \2139; color: var(--accent-blue); font-weight: bold; }
.alert-warning::before { content: \26A0; color: var(--accent-yellow); font-weight: bold; }
.alert-critical::before { content: \2757; color: var(--accent-red); font-weight: bold; }
.alert-suggestion::before { content: \1F4A1; color: var(--accent-green); }/* Footer / Status Bar */
.status-bar {display: flex;justify-content: space-between;align-items: center;padding: 0 var(--container-padding);height: var(--footer-height);border-top: 1px solid var(--border-color);background-color: #fbfbfd;font-size: 0.8em;color: var(--text-color-secondary);
}/* Scrollbar Styling (optional, Webkit) */
::-webkit-scrollbar {width: 6px;height: 6px;
}
::-webkit-scrollbar-track {background: #f1f1f1;border-radius: 3px;
}
::-webkit-scrollbar-thumb {background: #c1c1c1;border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {background: #a8a8a8;
}/* Responsive Adjustments */
media (max-width: 1200px) {.metrics {gap: 15px;}.metric-item .value {font-size: 1em;}
}media (max-width: 992px) {.main-content {flex-direction: column;min-height: auto;}.grid-visualization-section,.data-analysis-section {flex: none;width: 100%;}.topology-container {min-height: 250px;}.analysis-alerts-panel {max-height: 150px;}
}media (max-width: 768px) {body {padding: 10px;}.container {border-radius: 0;border-left: none;border-right: none;}.overview-bar {flex-direction: column;height: auto;padding: 10px var(--panel-padding);align-items: flex-start;}.overview-bar h1 {margin-bottom: 10px;}.metrics {width: 100%;justify-content: space-between;gap: 10px;}.metric-item {align-items: center; /* Center metrics on mobile */}.main-content {padding: var(--panel-padding);}.grid-visualization-section, .data-analysis-section {gap: var(--panel-padding);}.time-control-panel {display: flex;flex-wrap: wrap;gap: 10px;}.time-control-panel input,.time-control-panel select,.time-control-panel button {margin-right: 0;}
}media (max-width: 480px) {.metrics {flex-wrap: wrap;justify-content: center;}.metric-item {flex-basis: 45%;align-items: center;margin-bottom: 5px;}.overview-bar h1 {font-size: 1.1em;}
} script.js
document.addEventListener(DOMContentLoaded, () {// --- DOM Elements ---const currentTotalLoadSpan document.getElementById(currentTotalLoad);const predictedPeakLoadSpan document.getElementById(predictedPeakLoad);const gridStabilityIndexSpan document.getElementById(gridStabilityIndex);const topologyMapDiv document.getElementById(topologyMap);const currentDateTimeInput document.getElementById(currentDateTime);const playPauseBtn document.getElementById(playPauseBtn);const timeSpeedSelect document.getElementById(timeSpeed);const resetTimeBtn document.getElementById(resetTimeBtn);const loadForecastCanvas document.getElementById(loadForecastChart);const selectedNodeNameSpan document.getElementById(selectedNodeName);const nodeDetailsDiv document.getElementById(nodeDetails);const alertListUl document.getElementById(alertList);const simulationTimeSpan document.getElementById(simulationTime);const simulationStatusSpan document.getElementById(simulationStatus);// --- Simulation Configuration ---const config {startTime: dayjs().startOf(day).toDate(), // Start at beginning of todayupdateIntervalMs: 1000, // Real-time update intervalforecastHorizonHours: 24,historicalHours: 6, // How many hours of history to show on chartnodeClickHighlightDuration: 5000, // ms// Simplified grid structuregrid: {nodes: [{ id: S1, name: 主变电站 A, type: substation, x: 10, y: 50, capacity: 150 },{ id: S2, name: 变电站 B, type: substation, x: 40, y: 20, capacity: 100 },{ id: S3, name: 变电站 C, type: substation, x: 45, y: 80, capacity: 120 },{ id: F1, name: 馈线 1 (商业区), type: feeder, x: 70, y: 10, baseLoad: [10, 8, 7, 6, 7, 8, 15, 25, 35, 40, 45, 50, 48, 45, 42, 40, 38, 42, 48, 45, 35, 25, 18, 12], weekendMultiplier: 0.6, capacity: 60 },{ id: F2, name: 馈线 2 (工业区), type: feeder, x: 80, y: 45, baseLoad: [15, 12, 10, 10, 12, 15, 20, 30, 45, 55, 60, 60, 55, 50, 48, 45, 40, 35, 30, 25, 20, 18, 16, 15], weekendMultiplier: 0.4, capacity: 70 },{ id: F3, name: 馈线 3 (居民区), type: feeder, x: 75, y: 85, baseLoad: [8, 6, 5, 5, 6, 8, 12, 18, 25, 28, 30, 32, 30, 28, 25, 28, 35, 45, 50, 45, 35, 25, 15, 10], weekendMultiplier: 1.1, capacity: 60 },{ id: F4, name: 馈线 4 (混合区), type: feeder, x: 90, y: 65, baseLoad: [5, 4, 4, 4, 5, 7, 10, 15, 20, 22, 25, 26, 25, 24, 22, 23, 28, 35, 38, 35, 28, 20, 12, 8], weekendMultiplier: 0.9, capacity: 50 },],// Connections define power flow directionality for simulationconnections: [{ from: S1, to: S2 },{ from: S1, to: S3 },{ from: S2, to: F1 },{ from: S2, to: F2 },{ from: S3, to: F3 },{ from: S1, to: F4 } // Direct feeder from main substation]},loadFluctuationPercent: 5, // /- 5% random fluctuationvoltageFluctuationPercent: 1, // /- 1% random fluctuation from nominal 220kV/10kV etc.stabilityThresholds: { // For calculating stability indexoverloadCount: 3, // Max allowed overloaded nodes for high stabilityhighLoadCount: 5, // Max allowed high-load nodes}};// --- Simulation State ---let currentTime dayjs(config.startTime);let simulationRunning false;let simulationSpeed 1;let simulationTimer null;let gridState {}; // { nodeId: { load, voltage, status, forecast[...] }, ... }let selectedNodeId null;let nodeElements {}; // Store DOM elements for nodeslet lineElements {}; // Store DOM elements for lines// --- Chart Instance ---let loadForecastChart null;// --- Utility Functions ---function getRandom(min, max) {return Math.random() * (max - min) min;}function formatDateTime(date) {return dayjs(date).format(YYYY-MM-DD HH:mm:ss);}function formatLocalDateTimeForInput(date) {// HTML datetime-local input needs YYYY-MM-DDTHH:mmreturn dayjs(date).format(YYYY-MM-DDTHH:mm);}// *** NEW Helper function to aggregate data points ***function aggregateDataPoints(dataArrays) {const aggregatedMap new Map(); // Maptimestamp_ms, totalLoadconst timePoints new Set(); // Store unique timestamps in orderdataArrays.forEach(arr {if (!arr) return; // Skip if array is null or undefinedarr.forEach(point {if (!point || !point.time) return; // Skip invalid pointsconst timestampMs point.time.getTime();const currentLoad aggregatedMap.get(timestampMs) || 0;aggregatedMap.set(timestampMs, currentLoad point.load);timePoints.add(timestampMs);});});// Sort timestamps and create the final arrayconst sortedTimestamps Array.from(timePoints).sort((a, b) a - b);return sortedTimestamps.map(ts ({time: new Date(ts),load: aggregatedMap.get(ts)}));}// --- Initialization ---function initializeGridState() {gridState {};config.grid.nodes.forEach(node {gridState[node.id] {load: 0,voltage: node.type substation ? 220 : 10, // Simplified nominal voltage kVstatus: normal, // normal, low, high, overload, offlineforecast: [], // Array of { time, load }config: node // Reference to static config};});}function initializeChart() {if (loadForecastChart) loadForecastChart.destroy();const ctx loadForecastCanvas.getContext(2d);loadForecastChart new Chart(ctx, {type: line,data: {// labels: [], // Handled by time scaledatasets: [{label: 历史负载 (MW),data: [], // { x: time, y: load }borderColor: rgba(0, 122, 255, 0.8),backgroundColor: transparent,borderWidth: 2,pointRadius: 0,tension: 0.1},{label: 预测负载 (MW),data: [], // { x: time, y: load }borderColor: rgba(255, 149, 0, 0.8), // OrangebackgroundColor: transparent,borderDash: [5, 5], // Dashed line for forecastborderWidth: 2,pointRadius: 0,tension: 0.1}]},options: {responsive: true,maintainAspectRatio: false,animation: { duration: 0 }, // Disable animation for performancescales: {x: {type: time,time: {unit: hour,tooltipFormat: YYYY-MM-DD HH:mm, // Format for tooltipsdisplayFormats: { hour: HH:mm }},title: { display: true, text: 时间 },ticks: { source: auto, maxRotation: 0, autoSkipPadding: 20 }},y: {beginAtZero: true,title: { display: true, text: 负载 (MW) }}},plugins: {legend: { position: top },tooltip: { mode: index, intersect: false }},interaction: { mode: nearest, axis: x, intersect: false }}});}function drawGridTopology() {topologyMapDiv.innerHTML ; // Clear previousnodeElements {};lineElements {};const mapWidth topologyMapDiv.clientWidth;const mapHeight topologyMapDiv.clientHeight;// Create lines first (so they are behind nodes)config.grid.connections.forEach((conn, index) {const fromNode config.grid.nodes.find(n n.id conn.from);const toNode config.grid.nodes.find(n n.id conn.to);if (!fromNode || !toNode) return;const x1 (fromNode.x / 100) * mapWidth;const y1 (fromNode.y / 100) * mapHeight;const x2 (toNode.x / 100) * mapWidth;const y2 (toNode.y / 100) * mapHeight;const angle Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;const length Math.sqrt(Math.pow(x2 - x1, 2) Math.pow(y2 - y1, 2));const line document.createElement(div);line.classList.add(grid-line);line.style.left ${x1}px;line.style.top ${y1}px;line.style.width ${length}px;line.style.transform rotate(${angle}deg);const lineId line-${conn.from}-${conn.to};line.id lineId;lineElements[lineId] line;topologyMapDiv.appendChild(line);});// Create nodesconfig.grid.nodes.forEach(node {const nodeEl document.createElement(div);nodeEl.classList.add(grid-node);nodeEl.dataset.nodeId node.id;nodeEl.id node-${node.id};nodeEl.textContent node.id; // Simple ID displaynodeEl.title node.name; // TooltipnodeEl.style.left calc(${(node.x / 100) * 100}% - 15px); // Center the node (width/2)nodeEl.style.top calc(${(node.y / 100) * 100}% - 15px); // Center the node (height/2)if (node.type substation) {nodeEl.style.borderRadius 4px; // Square for substationsnodeEl.style.width 35px;nodeEl.style.height 35px;nodeEl.style.left calc(${(node.x / 100) * 100}% - 17.5px);nodeEl.style.top calc(${(node.y / 100) * 100}% - 17.5px);}nodeEl.addEventListener(click, () handleNodeClick(node.id));nodeElements[node.id] nodeEl;topologyMapDiv.appendChild(nodeEl);});}// --- Simulation Loop ---function simulationStep() {if (!simulationRunning) return;const timeIncrementSeconds 3600 * simulationSpeed; // Advance by hours based on speedcurrentTime dayjs(currentTime).add(timeIncrementSeconds, second);updateGridState(currentTime);updateUI();// Schedule next stepsimulationTimer setTimeout(simulationStep, config.updateIntervalMs);}function updateGridState(time) {const hourOfDay time.hour();const dayOfWeek time.day(); // 0 Sunday, 6 Saturdayconst isWeekend (dayOfWeek 0 || dayOfWeek 6);let totalLoad 0;let overloadCount 0;let highLoadCount 0;let activeNodes 0;config.grid.nodes.forEach(node {const state gridState[node.id];if (state.status offline) return;let currentBaseLoad 0;if (node.type feeder node.baseLoad) {currentBaseLoad node.baseLoad[hourOfDay] * (isWeekend ? node.weekendMultiplier : 1);} else if (node.type substation) {// Substation load is sum of loads it feeds (simplified)currentBaseLoad config.grid.connections.filter(c c.from node.id).reduce((sum, conn) sum (gridState[conn.to]?.load || 0), 0);}// Add fluctuationstate.load currentBaseLoad * (1 getRandom(-config.loadFluctuationPercent / 100, config.loadFluctuationPercent / 100));state.load Math.max(0, state.load); // Cannot be negative// Simulate voltage fluctuation (simple)const baseVoltage node.type substation ? 220 : 10;state.voltage baseVoltage * (1 getRandom(-config.voltageFluctuationPercent / 100, config.voltageFluctuationPercent / 100));// Determine status based on load vs capacityconst loadRatio node.capacity ? state.load / node.capacity : 0;if (loadRatio 1.0) {state.status overload;overloadCount;} else if (loadRatio 0.8) {state.status high;highLoadCount;} else if (loadRatio 0.3) {state.status low;} else {state.status normal;}if (node.type feeder) { // Only feeders contribute directly to total load in this modeltotalLoad state.load;}activeNodes;// Generate simple forecaststate.forecast generateSimpleForecast(node, time, config.forecastHorizonHours);});gridState.totalLoad totalLoad;gridState.stabilityIndex calculateStabilityIndex(overloadCount, highLoadCount, activeNodes);gridState.predictedPeak calculatePredictedPeak(config.forecastHorizonHours);// Generate Alerts based on current state and forecastgenerateAlerts(time);}function generateSimpleForecast(node, startTime, hours) {const forecast [];if (node.type ! feeder || !node.baseLoad) {// Simplified: Substations forecast is sum of feeder forecasts// For now, return empty forecast for non-feeders/nodes without baseLoadif (node.type substation) {const connectedFeeders config.grid.connections.filter(c c.from node.id gridState[c.to]?.config?.type feeder).map(c c.to);if (connectedFeeders.length 0) {for (let i 1; i hours; i) {const forecastTime dayjs(startTime).add(i, hour);let subForecastLoad 0;connectedFeeders.forEach(feederId {const feederNode gridState[feederId].config;const feederForecast generateSimpleForecast(feederNode, startTime, hours);subForecastLoad feederForecast[i-1]?.load || 0;});forecast.push({ time: forecastTime.toDate(), load: subForecastLoad });}return forecast;}}return [];}for (let i 1; i hours; i) {const forecastTime dayjs(startTime).add(i, hour);const hour forecastTime.hour();const day forecastTime.day();const weekend (day 0 || day 6);let forecastLoad node.baseLoad[hour] * (weekend ? node.weekendMultiplier : 1);// Add some basic trend/randomness if needed, keeping it simple hereforecastLoad * (1 getRandom(-config.loadFluctuationPercent / 150, config.loadFluctuationPercent / 150)); // Less fluctuation in forecastforecast.push({ time: forecastTime.toDate(), load: Math.max(0, forecastLoad) });}return forecast;}function calculatePredictedPeak(hours) {let peakLoad 0;let peakTime null;const forecastEndTime dayjs(currentTime).add(hours, hour);// Aggregate forecasts across all feederslet aggregatedForecast Array(hours).fill(0);config.grid.nodes.forEach(node {if (gridState[node.id]?.forecast?.length hours) {gridState[node.id].forecast.forEach((f, index) {if (node.type feeder) { // Only sum feeder forecasts for total peakaggregatedForecast[index] f.load;}});}});aggregatedForecast.forEach((load, index) {if (load peakLoad) {peakLoad load;peakTime dayjs(currentTime).add(index 1, hour);}});return { load: peakLoad, time: peakTime };}function calculateStabilityIndex(overloadCount, highLoadCount, activeNodes) {// Very simple conceptual index 0-100let score 100;score - overloadCount * 30; // Heavy penalty for overloadsscore - highLoadCount * 10; // Medium penalty for high loadif (activeNodes config.grid.nodes.length * 0.8) score - 20; // Penalty for offline nodesreturn Math.max(0, Math.min(100, Math.round(score)));}// --- UI Update ---function updateUI() {currentDateTimeInput.value formatLocalDateTimeForInput(currentTime);simulationTimeSpan.textContent formatDateTime(currentTime);simulationStatusSpan.textContent simulationRunning ? 运行中 : 已暂停;currentTotalLoadSpan.textContent ${gridState.totalLoad?.toFixed(1) ?? --} MW;predictedPeakLoadSpan.textContent ${gridState.predictedPeak?.load.toFixed(1) ?? --} MW;gridStabilityIndexSpan.textContent gridState.stabilityIndex ?? --;updateTopologyStyles();updateNodeDetailsPanel(); // Update panel if a node is selectedupdateLoadForecastChart(); // Update chart}function updateTopologyStyles() {config.grid.nodes.forEach(node {const el nodeElements[node.id];const state gridState[node.id];if (el state) {// Remove old status classesel.classList.remove(low, normal, high, overload, offline);// Add current status classel.classList.add(state.status);}});config.grid.connections.forEach(conn {const lineEl lineElements[line-${conn.from}-${conn.to}];const fromState gridState[conn.from];const toState gridState[conn.to];if (lineEl fromState toState) {lineEl.classList.remove(low, normal, high, overload, offline);// Line color based on the node it feeds (the to node), or from if to is offlineconst statusToUse (toState.status ! offline) ? toState.status : fromState.status;lineEl.classList.add(statusToUse);if (fromState.status offline || toState.status offline) {lineEl.classList.add(offline); // If either end is offline, line is offline}}});}function updateNodeDetailsPanel() {if (!selectedNodeId || !gridState[selectedNodeId]) {selectedNodeNameSpan.textContent 未选择;nodeDetailsDiv.innerHTML p请在左侧拓扑图中选择一个节点查看详细数据。/p;return;}const state gridState[selectedNodeId];const nodeConfig state.config;selectedNodeNameSpan.textContent nodeConfig.name;let detailsHTML pstrongID:/strong span${nodeConfig.id}/span/ppstrong类型:/strong span${nodeConfig.type substation ? 变电站 : 馈线}/span/ppstrong当前负载:/strong span classstatus-${state.status}${state.load.toFixed(1)} MW/span/ppstrong${nodeConfig.type substation ? 额定电压: : 馈线电压:}/strong span${state.voltage.toFixed(1)} kV/span/p${nodeConfig.capacity ? pstrong容量:/strong span${nodeConfig.capacity} MW/span/p : }pstrong状态:/strong span classstatus-${state.status}${getStatusText(state.status)}/span/p;// Add predicted peak for this specific node if availableif (state.forecast state.forecast.length 0) {const nodePeak state.forecast.reduce((max, p) p.load max.load ? p : max, { load: 0 });if (nodePeak.load 0) {detailsHTML pstrong预测峰值 (节点, 24h):/strong span${nodePeak.load.toFixed(1)} MW at ${dayjs(nodePeak.time).format(HH:mm)}/span/p;}}nodeDetailsDiv.innerHTML detailsHTML;}// *** MODIFIED function to show total load or selected node load ***function updateLoadForecastChart() {if (!loadForecastChart) return; // Chart not initializedlet historicalData [];let forecastData [];let chartLabelSuffix ;if (!selectedNodeId) {// No node selected - Show aggregated data for all feederschartLabelSuffix (总计);const allHistorical [];const allForecast [];config.grid.nodes.forEach(node {if (node.type feeder) {allHistorical.push(getHistoricalData(node.id, config.historicalHours));allForecast.push(gridState[node.id]?.forecast || []);}});historicalData aggregateDataPoints(allHistorical);forecastData aggregateDataPoints(allForecast);} else if (gridState[selectedNodeId]) {// Node selected - Show its specific dataconst state gridState[selectedNodeId];chartLabelSuffix (${state.config.id});historicalData getHistoricalData(selectedNodeId, config.historicalHours);forecastData state.forecast || [];} else {// Selected node ID exists but no state found (error case?)// Clear the chart}// Update chart datasetsloadForecastChart.data.datasets[0].data historicalData.map(p ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[0].label 历史负载 (MW)${chartLabelSuffix};loadForecastChart.data.datasets[1].data forecastData.map(p ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[1].label 预测负载 (MW)${chartLabelSuffix};// Adjust time axis only if there is dataif (historicalData.length 0 || forecastData.length 0) {const firstTime historicalData[0]?.time ?? forecastData[0]?.time;const lastTime forecastData[forecastData.length - 1]?.time ?? historicalData[historicalData.length - 1]?.time;if (firstTime lastTime) {loadForecastChart.options.scales.x.min dayjs(firstTime).subtract(30, minute).toDate();loadForecastChart.options.scales.x.max dayjs(lastTime).add(30, minute).toDate();} else {// Reset axes if no valid time dataloadForecastChart.options.scales.x.min null;loadForecastChart.options.scales.x.max null;}} else {// Reset axes if no data at allloadForecastChart.options.scales.x.min null;loadForecastChart.options.scales.x.max null;}loadForecastChart.update(none); // Use none to avoid potentially jerky updates when switching nodes}function getHistoricalData(nodeId, hoursBack) {// This is simplified - in a real app, this data would come from a backend/database// Here, we just simulate it by recalculating past loads based on the patternconst history [];const node gridState[nodeId].config;if (node.type ! feeder || !node.baseLoad) return []; // Only feeders have direct history in this modelfor (let i hoursBack; i 0; i--) {const pastTime dayjs(currentTime).subtract(i, hour);const hour pastTime.hour();const day pastTime.day();const weekend (day 0 || day 6);let pastLoad node.baseLoad[hour] * (weekend ? node.weekendMultiplier : 1);pastLoad * (1 getRandom(-config.loadFluctuationPercent / 100, config.loadFluctuationPercent / 100)); // Simulate past fluctuationhistory.push({ time: pastTime.toDate(), load: Math.max(0, pastLoad) });}// Add current pointhistory.push({ time: currentTime.toDate(), load: gridState[nodeId].load });return history;}function getStatusText(status) {switch (status) {case low: return 低负载;case normal: return 正常;case high: return 高负载;case overload: return 过载;case offline: return 离线;default: return 未知;}}function addLog(message, type info) {const li document.createElement(li);li.classList.add(alert-${type});li.textContent [${formatDateTime(currentTime)}] ${message};alertListUl.insertBefore(li, alertListUl.firstChild);if (alertListUl.children.length 20) { // Limit log sizealertListUl.removeChild(alertListUl.lastChild);}}function generateAlerts(time) {// Check for current overloadsconfig.grid.nodes.forEach(node {const state gridState[node.id];if (state.status overload) {addLog(严重警告: 节点 ${node.name} (${node.id}) 当前已过载! 负载: ${state.load.toFixed(1)} MW / ${node.capacity} MW, critical);}});// Check for predicted overloads (within next few hours)const predictionHorizonAlert 6; // Check for overloads in next 6 hoursconfig.grid.nodes.forEach(node {const state gridState[node.id];if (state.forecast node.capacity) {for(let i0; i predictionHorizonAlert i state.forecast.length; i) {const forecastPoint state.forecast[i];if (forecastPoint.load node.capacity) {addLog(预警: 节点 ${node.name} (${node.id}) 预计在 ${dayjs(forecastPoint.time).format(HH:mm)} 过载 (预测 ${forecastPoint.load.toFixed(1)} MW), warning);break; // Only log first predicted overload for this node}}}});// Stability suggestionif (gridState.stabilityIndex 60) {addLog(建议: 电网稳定性 (${gridState.stabilityIndex}) 偏低请关注高负载和过载节点。, suggestion);}}// --- Event Handlers ---function handleNodeClick(nodeId) {if (selectedNodeId nodeId) {selectedNodeId null; // Deselect if clicked againnodeElements[nodeId]?.classList.remove(selected);} else {if (selectedNodeId nodeElements[selectedNodeId]) {nodeElements[selectedNodeId].classList.remove(selected);}selectedNodeId nodeId;if (nodeElements[selectedNodeId]) {nodeElements[selectedNodeId].classList.add(selected);// Optional: remove highlight after some timesetTimeout(() {nodeElements[selectedNodeId]?.classList.remove(selected);if(selectedNodeId nodeId) { /* Check if still selected */ } // Keep selected logically}, config.nodeClickHighlightDuration);}}updateNodeDetailsPanel();updateLoadForecastChart(); // Update chart for the selected/deselected node}playPauseBtn.addEventListener(click, () {simulationRunning !simulationRunning;playPauseBtn.textContent simulationRunning ? ⏸️ : ▶️;simulationStatusSpan.textContent simulationRunning ? 运行中 : 已暂停;if (simulationRunning) {clearTimeout(simulationTimer);simulationStep(); // Start the loop immediatelyaddLog(模拟已开始, info);} else {clearTimeout(simulationTimer);addLog(模拟已暂停, info);}});timeSpeedSelect.addEventListener(change, (e) {simulationSpeed parseInt(e.target.value, 10);addLog(模拟速度设置为 ${simulationSpeed}x, info);});resetTimeBtn.addEventListener(click, () {simulationRunning false;clearTimeout(simulationTimer);currentTime dayjs(config.startTime);playPauseBtn.textContent ▶️;initializeGridState();selectedNodeId null; // Deselect nodeupdateGridState(currentTime); // Recalculate initial stateupdateUI();// *** Modify Reset: Clear chart data instead of re-initializing ***if (loadForecastChart) {loadForecastChart.data.datasets[0].data [];loadForecastChart.data.datasets[1].data [];// Update chart labels to default (total load)loadForecastChart.data.datasets[0].label 历史负载 (MW) (总计);loadForecastChart.data.datasets[1].label 预测负载 (MW) (总计);// Recalculate aggregated data for the reset time and updateconst allHistorical [];const allForecast [];config.grid.nodes.forEach(node {if (node.type feeder) {allHistorical.push(getHistoricalData(node.id, config.historicalHours));allForecast.push(gridState[node.id]?.forecast || []);}});const historicalData aggregateDataPoints(allHistorical);const forecastData aggregateDataPoints(allForecast);loadForecastChart.data.datasets[0].data historicalData.map(p ({ x: p.time, y: p.load }));loadForecastChart.data.datasets[1].data forecastData.map(p ({ x: p.time, y: p.load }));// Reset axesloadForecastChart.options.scales.x.min null;loadForecastChart.options.scales.x.max null;loadForecastChart.update(none); // Update immediately without animation} else {// If chart wasnt initialized somehow, initialize it nowinitializeChart();}// *** End chart modification for reset ***alertListUl.innerHTML li系统已重置/li; // Clear logsaddLog(模拟已重置到初始时间, info);// updateLoadForecastChart(); // No longer needed here, handled above});// Resize handler for topology redrawlet resizeTimeout;window.addEventListener(resize, () {clearTimeout(resizeTimeout);resizeTimeout setTimeout(() {drawGridTopology(); // Redraw topologyupdateTopologyStyles(); // Reapply stylesif(loadForecastChart) { // Trigger chart redrawloadForecastChart.resize();}}, 250);});// --- Initial Setup ---function initializeApp() {initializeGridState();drawGridTopology();initializeChart();updateGridState(currentTime); // Calculate initial state before first drawupdateUI();addLog(电网分析组件初始化完成, info);}initializeApp();
});