Files
zpark-ems/frontend/src/pages/Dashboard/components/EnergyFlow.tsx

97 lines
3.2 KiB
TypeScript
Raw Normal View History

import ReactECharts from 'echarts-for-react';
import { useEffect, useState } from 'react';
import { getEnergyFlow } from '../../../services/api';
import { Spin, Typography, Space } from 'antd';
import { FireOutlined } from '@ant-design/icons';
const { Text } = Typography;
interface Props {
realtime?: {
pv_power: number;
heatpump_power: number;
total_load: number;
grid_power: number;
};
}
export default function EnergyFlow({ realtime }: Props) {
const [flowData, setFlowData] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
getEnergyFlow()
.then((data: any) => setFlowData(data))
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const pv = realtime?.pv_power || 0;
const hp = realtime?.heatpump_power || 0;
const load = realtime?.total_load || 0;
const grid = realtime?.grid_power || 0;
// Build sankey from realtime data as fallback if API has no flow data
const pvToBuilding = Math.min(pv, load);
const pvToGrid = Math.max(0, pv - load);
const gridToBuilding = Math.max(0, load - pv);
const gridToHeatPump = hp;
const links = flowData?.links || [
{ source: '光伏发电', target: '建筑用电', value: pvToBuilding || 0.1 },
{ source: '光伏发电', target: '电网输出', value: pvToGrid || 0.1 },
{ source: '电网输入', target: '建筑用电', value: gridToBuilding || 0.1 },
{ source: '电网输入', target: '热泵系统', value: gridToHeatPump || 0.1 },
].filter((l: any) => l.value > 0.05);
const nodes = flowData?.nodes || [
{ name: '光伏发电', itemStyle: { color: '#faad14' } },
{ name: '电网输入', itemStyle: { color: '#52c41a' } },
{ name: '建筑用电', itemStyle: { color: '#1890ff' } },
{ name: '电网输出', itemStyle: { color: '#13c2c2' } },
{ name: '热泵系统', itemStyle: { color: '#f5222d' } },
];
// Only show nodes that appear in links
const usedNames = new Set<string>();
links.forEach((l: any) => { usedNames.add(l.source); usedNames.add(l.target); });
const filteredNodes = nodes.filter((n: any) => usedNames.has(n.name));
const option = {
tooltip: { trigger: 'item', triggerOn: 'mousemove' },
series: [{
type: 'sankey',
layout: 'none',
emphasis: { focus: 'adjacency' },
nodeAlign: 'left',
orient: 'horizontal',
top: 10,
bottom: 30,
left: 10,
right: 10,
nodeWidth: 20,
nodeGap: 16,
data: filteredNodes,
links: links,
label: { fontSize: 12 },
lineStyle: { color: 'gradient', curveness: 0.5 },
}],
};
if (loading) return <Spin style={{ display: 'block', margin: '80px auto' }} />;
return (
<div>
<ReactECharts option={option} style={{ height: 240 }} />
<div style={{ textAlign: 'center', padding: '4px 8px', background: '#fafafa', borderRadius: 8 }}>
<Space size={24}>
<span><FireOutlined style={{ color: '#f5222d' }} /> : <Text strong>{hp.toFixed(1)} kW</Text></span>
<span>: <Text strong style={{ color: '#52c41a' }}>
{load > 0 ? ((Math.min(pv, load) / load) * 100).toFixed(1) : 0}%
</Text></span>
</Space>
</div>
</div>
);
}