97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|