feat: customer frontend, Sungrow collector fixes, real data (v1.2.0)
- Add frontend/ at root (no Three.js, no Charging, green #52c41a theme) - Fix Sungrow collector: add curPage/size params, unit conversion - Fix station-level dedup to prevent double-counting - Add shared token cache for API rate limit protection - Add .githooks/pre-commit, CLAUDE.md, .gitignore - Update docker-compose.override.yml frontend -> ./frontend - Pin bcrypt in requirements.txt - Add BUYOFF_RESULTS_2026-04-05.md (39/43 pass) - Data accuracy: 0.0% diff vs iSolarCloud Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
frontend/src/pages/Dashboard/components/EnergyFlow.tsx
Normal file
96
frontend/src/pages/Dashboard/components/EnergyFlow.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user