光伏发电园区管理系统 - Three.js + Django 实现方案
我将设计一个基于Three.js和Django的光伏发电园区管理系统,包含3D可视化、实时监控和数据分析功能。
系统架构设计
技术栈与依赖
前端:
- Three.js (r128) - 3D渲染
- Chart.js - 数据可视化
- Bootstrap 5 - UI框架
- WebSocket - 实时数据更新
后端:
- Django 4.0
- Django REST Framework
- Django Channels (WebSocket支持)
- PostgreSQL (带PostGIS扩展)
- Celery (任务队列)
其他依赖:
- pandas - 数据分析
- numpy - 数值计算
- requests - 外部API调用
实现步骤
1. Django 项目设置
# 创建虚拟环境
python -m venv env
source env/bin/activate
# 安装依赖
pip install django djangorestframework django-channels channels-redis celery pandas numpy requests psycopg2
# 创建项目
django-admin startproject pv_management
cd pv_management
django-admin startapp dashboard
2. 数据库模型设计 (dashboard/models.py
)
from django.contrib.gis.db import models
from django.contrib.postgres.fields import JSONField
class SolarPark(models.Model):
name = models.CharField(max_length=100)
location = models.PointField()
capacity = models.FloatField() # 总容量 (MW)
installation_date = models.DateField()
def __str__(self):
return self.name
class PanelArray(models.Model):
park = models.ForeignKey(SolarPark, on_delete=models.CASCADE, related_name='arrays')
array_id = models.CharField(max_length=20)
panel_type = models.CharField(max_length=50)
panel_count = models.IntegerField()
orientation = models.FloatField() # 方位角 (0-360)
tilt = models.FloatField() # 倾斜角 (0-90)
position = models.PointField()
installation_date = models.DateField()
def __str__(self):
return f"{self.park.name} - Array {self.array_id}"
class PanelData(models.Model):
array = models.ForeignKey(PanelArray, on_delete=models.CASCADE, related_name='data')
timestamp = models.DateTimeField(auto_now_add=True)
temperature = models.FloatField() # 面板温度 (°C)
voltage = models.FloatField() # 输出电压 (V)
current = models.FloatField() # 输出电流 (A)
power = models.FloatField() # 输出功率 (W)
efficiency = models.FloatField() # 转换效率 (%)
status = models.CharField(max_length=20, choices=[
('normal', '正常'),
('degraded', '性能下降'),
('fault', '故障'),
('maintenance', '维护中')
])
weather_data = JSONField(null=True, blank=True) # 存储天气数据
@property
def energy(self):
# 计算发电量 (kWh)
return (self.power * 0.001) # 假设1小时数据
def __str__(self):
return f"{self.array} @ {self.timestamp}"
class MaintenanceLog(models.Model):
array = models.ForeignKey(PanelArray, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
description = models.TextField()
technician = models.CharField(max_length=100)
resolved = models.BooleanField(default=False)
def __str__(self):
return f"维护记录 - {self.array} @ {self.timestamp}"
3. Django 视图与API (dashboard/views.py
)
from rest_framework import viewsets, generics
from rest_framework.response import Response
from .models import SolarPark, PanelArray, PanelData, MaintenanceLog
from .serializers import (
SolarParkSerializer,
PanelArraySerializer,
PanelDataSerializer,
MaintenanceLogSerializer
)
from django.contrib.gis.geos import Point
from django.utils import timezone
from datetime import timedelta
import pandas as pd
class SolarParkViewSet(viewsets.ModelViewSet):
queryset = SolarPark.objects.all()
serializer_class = SolarParkSerializer
class PanelArrayViewSet(viewsets.ModelViewSet):
queryset = PanelArray.objects.all()
serializer_class = PanelArraySerializer
class PanelDataViewSet(viewsets.ModelViewSet):
queryset = PanelData.objects.all()
serializer_class = PanelDataSerializer
class MaintenanceLogViewSet(viewsets.ModelViewSet):
queryset = MaintenanceLog.objects.all()
serializer_class = MaintenanceLogSerializer
class ParkSummaryAPI(generics.RetrieveAPIView):
def get(self, request, park_id):
park = SolarPark.objects.get(id=park_id)
arrays = park.arrays.all()
# 当前发电数据
current_data = PanelData.objects.filter(
array__park=park,
timestamp__gte=timezone.now()-timedelta(minutes=5)
).order_by('-timestamp')
# 计算总发电量
total_power = sum([d.power for d in current_data])
total_energy = sum([d.energy for d in current_data])
# 效率分析
efficiencies = [d.efficiency for d in current_data]
avg_efficiency = sum(efficiencies) / len(efficiencies) if efficiencies else 0
# 状态统计
status_counts = current_data.values('status').annotate(count=models.Count('id'))
return Response({
'park': SolarParkSerializer(park).data,
'total_power': total_power,
'total_energy': total_energy,
'avg_efficiency': avg_efficiency,
'status_distribution': status_counts,
'arrays': PanelArraySerializer(arrays, many=True).data
})
class HistoricalDataAPI(generics.RetrieveAPIView):
def get(self, request, array_id):
time_range = request.GET.get('range', '24h') # 默认24小时
if time_range == '24h':
delta = timedelta(hours=24)
elif time_range == '7d':
delta = timedelta(days=7)
elif time_range == '30d':
delta = timedelta(days=30)
else:
delta = timedelta(hours=24)
end_time = timezone.now()
start_time = end_time - delta
data = PanelData.objects.filter(
array_id=array_id,
timestamp__range=(start_time, end_time)
).order_by('timestamp')
# 使用Pandas进行数据处理
df = pd.DataFrame.from_records(data.values(
'timestamp', 'power', 'voltage', 'current', 'efficiency', 'temperature'
))
# 按小时聚合
df['timestamp'] = pd.to_datetime(df['timestamp'])
df.set_index('timestamp', inplace=True)
hourly = df.resample('H').mean()
return Response({
'raw_data': PanelDataSerializer(data, many=True).data,
'hourly_data': hourly.reset_index().to_dict(orient='records')
})
4. Three.js 光伏园区可视化 (frontend/js/solar_visualization.js
)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
class SolarParkVisualization {
constructor(containerId, parkData) {
this.container = document.getElementById(containerId);
this.parkData = parkData;
// 场景设置
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x87CEEB); // 天空蓝
// 相机设置
this.camera = new THREE.PerspectiveCamera(
60,
this.container.clientWidth / this.container.clientHeight,
0.1,
10000
);
this.camera.position.set(0, 200, 300);
// 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.container.appendChild(this.renderer.domElement);
// 控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
// 添加灯光
this.addLights();
// 加载环境
this.loadEnvironment();
// 创建光伏阵列
this.createSolarArrays();
// 添加后期处理
this.setupPostProcessing();
// 事件监听
window.addEventListener('resize', this.onWindowResize.bind(this));
this.renderer.domElement.addEventListener('click', this.onCanvasClick.bind(this));
// 动画循环
this.animate();
}
addLights() {
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
// 方向光(模拟太阳)
this.sunLight = new THREE.DirectionalLight(0xffffff, 1.0);
this.sunLight.position.set(100, 200, 100);
this.sunLight.castShadow = true;
this.sunLight.shadow.mapSize.width = 2048;
this.sunLight.shadow.mapSize.height = 2048;
this.scene.add(this.sunLight);
// 辅助光
const fillLight = new THREE.DirectionalLight(0xffffff, 0.5);
fillLight.position.set(-100, 100, -100);
this.scene.add(fillLight);
}
loadEnvironment() {
// 添加地面
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x8B4513,
roughness: 0.9,
metalness: 0.1
});
this.ground = new THREE.Mesh(groundGeometry, groundMaterial);
this.ground.rotation.x = -Math.PI / 2;
this.ground.receiveShadow = true;
this.scene.add(this.ground);
// 加载天空盒
const loader = new THREE.CubeTextureLoader();
const texture = loader.load([
'textures/sky/px.jpg', 'textures/sky/nx.jpg',
'textures/sky/py.jpg', 'textures/sky/ny.jpg',
'textures/sky/pz.jpg', 'textures/sky/nz.jpg'
]);
this.scene.background = texture;
// 添加简单树木
this.addVegetation();
}
addVegetation() {
const treeGeometry = new THREE.ConeGeometry(5, 15, 8);
const treeMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 });
for (let i = 0; i < 20; i++) {
const tree = new THREE.Mesh(treeGeometry, treeMaterial);
tree.position.set(
Math.random() * 400 - 200,
7.5,
Math.random() * 400 - 200
);
tree.castShadow = true;
this.scene.add(tree);
}
}
createSolarArrays() {
this.panelArrays = [];
this.parkData.arrays.forEach(arrayData => {
const arrayGroup = new THREE.Group();
arrayGroup.name = `array-${arrayData.id}`;
arrayGroup.userData = arrayData;
// 根据方位和倾斜角计算方向
const rotationY = THREE.MathUtils.degToRad(arrayData.orientation);
const rotationX = THREE.MathUtils.degToRad(arrayData.tilt);
// 计算位置
const position = new THREE.Vector3(
arrayData.position.x,
1, // 离地高度
arrayData.position.y
);
// 创建光伏板
const rows = Math.ceil(Math.sqrt(arrayData.panel_count));
const cols = Math.ceil(arrayData.panel_count / rows);
const panelWidth = 1.0;
const panelHeight = 1.7;
const spacing = 0.1;
const panelGeometry = new THREE.BoxGeometry(panelWidth, 0.05, panelHeight);
const panelMaterial = new THREE.MeshPhysicalMaterial({
color: 0x333333,
metalness: 0.9,
roughness: 0.1,
transparent: true,
opacity: 0.9,
emissive: 0x111111
});
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const panelIndex = i * cols + j;
if (panelIndex >= arrayData.panel_count) break;
const panel = new THREE.Mesh(panelGeometry, panelMaterial);
panel.castShadow = true;
panel.receiveShadow = true;
// 计算位置
panel.position.x = (i - rows/2) * (panelWidth + spacing);
panel.position.z = (j - cols/2) * (panelHeight + spacing);
// 添加玻璃表面
const glassGeometry = new THREE.PlaneGeometry(panelWidth * 0.95, panelHeight * 0.95);
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0x00aaff,
metalness: 0.1,
roughness: 0.05,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
});
const glass = new THREE.Mesh(glassGeometry, glassMaterial);
glass.position.y = 0.026; // 稍微高于面板
glass.rotation.x = Math.PI / 2;
panel.add(glass);
arrayGroup.add(panel);
}
}
// 设置阵列位置和旋转
arrayGroup.position.copy(position);
arrayGroup.rotation.y = rotationY;
arrayGroup.rotation.x = rotationX;
// 添加支撑结构
const supportGeometry = new THREE.BoxGeometry(0.1, 1.5, 0.1);
const supportMaterial = new THREE.MeshStandardMaterial({ color: 0xAAAAAA });
const supportPositions = [
new THREE.Vector3(-(rows/2 * (panelWidth+spacing)), -0.75, -(cols/2 * (panelHeight+spacing))),
new THREE.Vector3( (rows/2 * (panelWidth+spacing)), -0.75, -(cols/2 * (panelHeight+spacing))),
new THREE.Vector3(-(rows/2 * (panelWidth+spacing)), -0.75, (cols/2 * (panelHeight+spacing))),
new THREE.Vector3( (rows/2 * (panelWidth+spacing)), -0.75, (cols/2 * (panelHeight+spacing)))
];
supportPositions.forEach(pos => {
const support = new THREE.Mesh(supportGeometry, supportMaterial);
support.position.copy(pos);
support.position.y = -0.75;
arrayGroup.add(support);
});
this.scene.add(arrayGroup);
this.panelArrays.push(arrayGroup);
});
}
setupPostProcessing() {
this.composer = new EffectComposer(this.renderer);
const renderPass = new RenderPass(this.scene, this.camera);
this.composer.addPass(renderPass);
// 轮廓效果
this.outlinePass = new OutlinePass(
new THREE.Vector2(this.container.clientWidth, this.container.clientHeight),
this.scene,
this.camera
);
this.outlinePass.visibleEdgeColor.set(0x00ff00);
this.outlinePass.hiddenEdgeColor.set(0x000000);
this.outlinePass.edgeStrength = 3.0;
this.outlinePass.edgeThickness = 1.0;
this.outlinePass.edgeGlow = 0.5;
this.composer.addPass(this.outlinePass);
}
onWindowResize() {
this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.composer.setSize(this.container.clientWidth, this.container.clientHeight);
}
onCanvasClick(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1;
mouse.y = - (event.clientY / this.renderer.domElement.clientHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(this.panelArrays);
if (intersects.length > 0) {
const selectedArray = intersects[0].object.parent;
this.outlinePass.selectedObjects = [selectedArray];
// 触发自定义事件
const arraySelected = new CustomEvent('arraySelected', {
detail: { arrayData: selectedArray.userData }
});
document.dispatchEvent(arraySelected);
} else {
this.outlinePass.selectedObjects = [];
}
}
updatePanelStatus(panelData) {
this.panelArrays.forEach(arrayGroup => {
const arrayData = arrayGroup.userData;
// 查找该阵列的最新数据
const data = panelData.find(d => d.array_id === arrayData.id);
if (!data) return;
// 根据状态更新颜色
let color;
switch(data.status) {
case 'normal':
color = 0x00ff00; // 绿色
break;
case 'degraded':
color = 0xffff00; // 黄色
break;
case 'fault':
color = 0xff0000; // 红色
break;
case 'maintenance':
color = 0x0000ff; // 蓝色
break;
default:
color = 0xffffff; // 白色
}
// 更新面板材质
arrayGroup.traverse(child => {
if (child.isMesh && child.material instanceof THREE.MeshPhysicalMaterial) {
child.material.emissive = new THREE.Color(color);
child.material.emissiveIntensity = data.status === 'normal' ? 0.1 : 0.5;
// 对于玻璃部分
if (child.children.length > 0) {
const glass = child.children[0];
if (data.status === 'normal') {
glass.material.color.set(0x00aaff);
} else {
glass.material.color.set(0xff0000);
}
}
}
});
});
}
animate() {
requestAnimationFrame(this.animate.bind(this));
// 更新控制器
this.controls.update();
// 更新太阳位置
const time = Date.now() * 0.0001;
this.sunLight.position.x = Math.sin(time) * 200;
this.sunLight.position.z = Math.cos(time) * 200;
// 渲染
this.composer.render();
}
}
export default SolarParkVisualization;
5. 前端主界面 (frontend/index.html
)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光伏发电园区管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 侧边导航 -->
<div class="col-md-2 bg-dark text-white p-3">
<h4 class="text-center mb-4">光伏园区管理</h4>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#">园区概览</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">设备监控</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">维护管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">数据分析</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">系统设置</a>
</li>
</ul>
<div class="mt-5">
<h5>园区选择</h5>
<select id="park-select" class="form-select">
<option value="1">园区1 - 50MW</option>
<option value="2">园区2 - 30MW</option>
<option value="3">园区3 - 20MW</option>
</select>
</div>
<div class="mt-4">
<h5>实时状态</h5>
<div class="d-flex justify-content-between">
<span>总发电量:</span>
<span id="total-power">0 kW</span>
</div>
<div class="d-flex justify-content-between">
<span>总效率:</span>
<span id="total-efficiency">0%</span>
</div>
<div class="mt-2">
<div class="progress">
<div id="capacity-bar" class="progress-bar" role="progressbar"></div>
</div>
<small class="text-muted">容量使用率</small>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="col-md-10 p-0">
<div class="row">
<!-- 3D可视化区域 -->
<div class="col-md-8 p-0 position-relative">
<div id="solar-visualization" class="h-100"></div>
<!-- 控制面板 -->
<div class="position-absolute top-0 end-0 m-3">
<div class="btn-group">
<button class="btn btn-light" id="reset-view">
<i class="bi bi-arrow-clockwise"></i> 重置视图
</button>
<button class="btn btn-light" id="day-mode">
<i class="bi bi-sun"></i> 白天模式
</button>
<button class="btn btn-dark" id="night-mode">
<i class="bi bi-moon"></i> 夜间模式
</button>
</div>
</div>
</div>
<!-- 数据面板 -->
<div class="col-md-4 p-3">
<div class="card mb-3">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">选中的光伏阵列</h5>
</div>
<div class="card-body" id="array-details">
<p class="text-center text-muted">请选择光伏阵列查看详情</p>
</div>
</div>
<div class="card mb-3">
<div class="card-header bg-success text-white">
<h5 class="mb-0">实时发电数据</h5>
</div>
<div class="card-body">
<canvas id="power-chart" height="200"></canvas>
</div>
</div>
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">状态分布</h5>
</div>
<div class="card-body">
<canvas id="status-chart" height="200"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript 依赖 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
<script src="{% static 'js/solar_visualization.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
6. 前端主逻辑 (frontend/js/main.js
)
// 初始化变量
let solarViz;
let powerChart;
let statusChart;
let selectedArray = null;
const SOCKET_URL = `ws://${window.location.host}/ws/dashboard/`;
// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
// 加载园区数据
loadParkData(1);
// 设置事件监听器
document.getElementById('park-select').addEventListener('change', function() {
loadParkData(this.value);
});
document.addEventListener('arraySelected', function(e) {
selectedArray = e.detail.arrayData;
updateArrayDetails();
loadArrayData(selectedArray.id);
});
document.getElementById('reset-view').addEventListener('click', function() {
if (solarViz) {
solarViz.camera.position.set(0, 200, 300);
solarViz.camera.lookAt(0, 0, 0);
solarViz.controls.reset();
}
});
document.getElementById('day-mode').addEventListener('click', function() {
if (solarViz) {
solarViz.scene.background = new THREE.Color(0x87CEEB);
}
});
document.getElementById('night-mode').addEventListener('click', function() {
if (solarViz) {
solarViz.scene.background = new THREE.Color(0x0a0a2a);
}
});
// 初始化WebSocket连接
initWebSocket();
});
// 加载园区数据
function loadParkData(parkId) {
fetch(`/api/parks/${parkId}/summary/`)
.then(response => response.json())
.then(data => {
// 更新UI
document.getElementById('total-power').textContent =
`${(data.total_power / 1000).toFixed(1)} MW`;
document.getElementById('total-efficiency').textContent =
`${data.avg_efficiency.toFixed(1)}%`;
const capacityPercent = (data.total_power / (data.park.capacity * 1000000)) * 100;
document.getElementById('capacity-bar').style.width = `${capacityPercent}%`;
// 初始化3D可视化
initSolarVisualization(data);
// 初始化图表
initCharts(data);
});
}
// 初始化3D可视化
function initSolarVisualization(parkData) {
const container = document.getElementById('solar-visualization');
if (solarViz) {
container.innerHTML = '';
}
solarViz = new SolarParkVisualization('solar-visualization', parkData);
}
// 初始化图表
function initCharts(parkData) {
const powerCtx = document.getElementById('power-chart').getContext('2d');
// 销毁现有图表
if (powerChart) {
powerChart.destroy();
}
// 创建功率图表
powerChart = new Chart(powerCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '总功率 (kW)',
data: [],
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
// 创建状态图表
const statusCtx = document.getElementById('status-chart').getContext('2d');
if (statusChart) {
statusChart.destroy();
}
const statusData = parkData.status_distribution.map(s => s.count);
const statusLabels = parkData.status_distribution.map(s => {
switch(s.status) {
case 'normal': return '正常';
case 'degraded': return '性能下降';
case 'fault': return '故障';
case 'maintenance': return '维护中';
default: return s.status;
}
});
statusChart = new Chart(statusCtx, {
type: 'doughnut',
data: {
labels: statusLabels,
datasets: [{
data: statusData,
backgroundColor: [
'rgba(75, 192, 192, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// 更新阵列详情
function updateArrayDetails() {
if (!selectedArray) return;
const detailsDiv = document.getElementById('array-details');
detailsDiv.innerHTML = `
<h6>${selectedArray.park.name} - 阵列 ${selectedArray.array_id}</h6>
<hr>
<div class="row">
<div class="col-6">
<p><strong>面板类型:</strong></p>
<p>${selectedArray.panel_type}</p>
</div>
<div class="col-6">
<p><strong>面板数量:</strong></p>
<p>${selectedArray.panel_count}</p>
</div>
</div>
<div class="row mt-2">
<div class="col-6">
<p><strong>方位角:</strong></p>
<p>${selectedArray.orientation}°</p>
</div>
<div class="col-6">
<p><strong>倾斜角:</strong></p>
<p>${selectedArray.tilt}°</p>
</div>
</div>
<div class="mt-3">
<button class="btn btn-warning btn-sm">查看详情</button>
<button class="btn btn-danger btn-sm">报告问题</button>
</div>
`;
}
// 加载阵列数据
function loadArrayData(arrayId) {
fetch(`/api/arrays/${arrayId}/data/?range=24h`)
.then(response => response.json())
.then(data => {
// 更新图表
updatePowerChart(data.hourly_data);
});
}
// 更新功率图表
function updatePowerChart(hourlyData) {
if (!powerChart) return;
const labels = hourlyData.map(d => new Date(d.timestamp).toLocaleTimeString());
const powerValues = hourlyData.map(d => d.power ? d.power / 1000 : 0);
powerChart.data.labels = labels;
powerChart.data.datasets[0].data = powerValues;
powerChart.update();
}
// 初始化WebSocket
function initWebSocket() {
const socket = new WebSocket(SOCKET_URL);
socket.onopen = function() {
console.log('WebSocket连接已建立');
};
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
// 更新实时数据
if (data.type === 'realtime_update') {
document.getElementById('total-power').textContent =
`${(data.total_power / 1000).toFixed(1)} MW`;
document.getElementById('total-efficiency').textContent =
`${data.avg_efficiency.toFixed(1)}%`;
const capacityPercent = (data.total_power / (data.park_capacity * 1000000)) * 100;
document.getElementById('capacity-bar').style.width = `${capacityPercent}%`;
// 更新3D场景中的面板状态
if (solarViz) {
solarViz.updatePanelStatus(data.panel_data);
}
// 更新状态图表
if (statusChart) {
const statusData = data.status_distribution.map(s => s.count);
statusChart.data.datasets[0].data = statusData;
statusChart.update();
}
}
};
socket.onclose = function() {
console.log('WebSocket连接已关闭,5秒后重试...');
setTimeout(initWebSocket, 5000);
};
}
系统功能亮点
三维光伏园区可视化:
- 真实感光伏板阵列渲染
- 面板状态实时颜色编码(正常、故障、维护等)
- 交互式相机控制
实时数据监控:
- WebSocket实时数据推送
- 功率、效率、温度等关键指标监控
- 状态分布饼图
数据分析功能:
- 历史发电数据分析
- 效率趋势图表
- 容量利用率监控
设备管理:
- 光伏阵列详细信息查看
- 维护记录跟踪
- 故障报告功能
安装与部署指南
1. 环境准备
# 安装PostgreSQL
sudo apt install postgresql postgresql-contrib postgis
# 创建数据库
sudo -u postgres createdb pv_management
sudo -u postgres psql -c "CREATE USER pvuser WITH PASSWORD 'password';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pv_management TO pvuser;"
sudo -u postgres psql -d pv_management -c "CREATE EXTENSION postgis;"
2. 配置Django项目
# settings.py 配置
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'pv_management',
'USER': 'pvuser',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# 添加应用
INSTALLED_APPS = [
...
'channels',
'dashboard',
'django.contrib.gis',
]
# 配置ASGI
ASGI_APPLICATION = 'pv_management.asgi.application'
# 配置Channels
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [("127.0.0.1", 6379)],
},
},
}
# 静态文件配置
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'frontend/static')]
3. 运行系统
# 数据库迁移
python manage.py makemigrations
python manage.py migrate
# 收集静态文件
python manage.py collectstatic
# 启动Django开发服务器
python manage.py runserver
# 启动Celery worker (在另一个终端)
celery -A pv_management worker -l info
# 启动Channels (在另一个终端)
daphne pv_management.asgi:application
未来扩展方向
AI预测功能:
- 发电量预测
- 故障预测
- 清洁优化建议
无人机巡检集成:
- 自动巡检路径规划
- 热成像故障检测
- 基于图像的污垢分析
AR移动应用:
- 现场设备信息叠加
- 维护指导可视化
- 远程专家协助
能源交易平台:
- 实时电价监控
- 自动售电策略
- 区块链能源交易
这个系统为光伏发电园区提供了全面的数字化管理解决方案,结合了Three.js的强大可视化能力和Django的灵活后端处理,实现了光伏发电园区的智能化管理。