Files
ems-core/frontend/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx
Du Wenbo 92ec910a13 ems-core v1.0.0: Standard EMS platform core
Shared backend + frontend for multi-customer EMS deployments.
- 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc.
- 120+ API endpoints, 37 database tables
- Customer config mechanism (CUSTOMER env var + YAML config)
- Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud
- Frontend: React 19 + Ant Design + ECharts + Three.js
- Infrastructure: Redis cache, rate limiting, aggregation engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:14:11 +08:00

179 lines
5.7 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import styles from '../styles.module.css';
import { getDeviceRealtime } from '../../../services/api';
import { getDevicePhoto } from '../../../utils/devicePhoto';
interface Device {
id: number;
name: string;
code: string;
device_type: string;
status: string;
model?: string;
manufacturer?: string;
rated_power?: number;
}
interface DeviceInfoPanelProps {
device: Device | null;
onClose: () => void;
onViewDetail: (device: Device) => void;
}
interface ParamDef {
key: string;
label: string;
unit: string;
}
const PARAMS_BY_TYPE: Record<string, ParamDef[]> = {
pv_inverter: [
{ key: 'power', label: '功率', unit: 'kW' },
{ key: 'daily_energy', label: '日发电量', unit: 'kWh' },
{ key: 'total_energy', label: '累计发电', unit: 'kWh' },
{ key: 'dc_voltage', label: '直流电压', unit: 'V' },
{ key: 'ac_voltage', label: '交流电压', unit: 'V' },
{ key: 'temperature', label: '温度', unit: '℃' },
],
heat_pump: [
{ key: 'power', label: '功率', unit: 'kW' },
{ key: 'cop', label: 'COP', unit: '' },
{ key: 'inlet_temp', label: '进水温度', unit: '℃' },
{ key: 'outlet_temp', label: '出水温度', unit: '℃' },
{ key: 'flow_rate', label: '流量', unit: 'm³/h' },
{ key: 'outdoor_temp', label: '室外温度', unit: '℃' },
],
meter: [
{ key: 'power', label: '功率', unit: 'kW' },
{ key: 'voltage', label: '电压', unit: 'V' },
{ key: 'current', label: '电流', unit: 'A' },
{ key: 'power_factor', label: '功率因数', unit: '' },
],
sensor: [
{ key: 'temperature', label: '温度', unit: '℃' },
{ key: 'humidity', label: '湿度', unit: '%' },
],
heat_meter: [
{ key: 'heat_power', label: '热功率', unit: 'kW' },
{ key: 'flow_rate', label: '流量', unit: 'm³/h' },
{ key: 'supply_temp', label: '供水温度', unit: '℃' },
{ key: 'return_temp', label: '回水温度', unit: '℃' },
{ key: 'cumulative_heat', label: '累计热量', unit: 'GJ' },
],
};
const STATUS_COLORS: Record<string, string> = {
online: '#00ff88',
offline: '#666666',
alarm: '#ff4757',
maintenance: '#ff8c00',
};
const STATUS_LABELS: Record<string, string> = {
online: '在线',
offline: '离线',
alarm: '告警',
maintenance: '维护',
};
export default function DeviceInfoPanel({ device, onClose, onViewDetail }: DeviceInfoPanelProps) {
const [realtimeData, setRealtimeData] = useState<Record<string, { value: number; unit: string; timestamp: string }>>({});
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (!device) {
setRealtimeData({});
return;
}
const fetchData = async () => {
try {
const resp = await getDeviceRealtime(device.id) as any;
// API returns { device: {...}, data: { power: {...}, ... } }
const realtimeMap = resp?.data ?? resp;
setRealtimeData(realtimeMap as Record<string, { value: number; unit: string; timestamp: string }>);
} catch {
// ignore fetch errors
}
};
fetchData();
timerRef.current = setInterval(fetchData, 5000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [device?.id]);
if (!device) return null;
const params = PARAMS_BY_TYPE[device.device_type] || [];
return (
<div className={styles.deviceInfoPanel}>
<div className={styles.infoPanelHeader}>
<span className={styles.infoPanelTitle}>{device.name}</span>
<button className={styles.closeBtn} onClick={onClose}></button>
</div>
<div style={{ padding: '0 12px 8px', textAlign: 'center' }}>
<img src={getDevicePhoto(device.device_type)} alt={device.name}
style={{ width: '100%', height: 120, borderRadius: 8, objectFit: 'cover', border: '1px solid rgba(0,212,255,0.2)' }} />
</div>
<div>
<div className={styles.paramRow}>
<span className={styles.paramLabel}></span>
<span
style={{
padding: '2px 10px',
borderRadius: 4,
fontSize: 12,
fontWeight: 600,
color: '#fff',
backgroundColor: STATUS_COLORS[device.status] || '#666',
}}
>
{STATUS_LABELS[device.status] || device.status}
</span>
</div>
{device.model && (
<div className={styles.paramRow}>
<span className={styles.paramLabel}></span>
<span className={styles.paramValue}>{device.model}</span>
</div>
)}
{device.manufacturer && (
<div className={styles.paramRow}>
<span className={styles.paramLabel}></span>
<span className={styles.paramValue}>{device.manufacturer}</span>
</div>
)}
{device.rated_power != null && (
<div className={styles.paramRow}>
<span className={styles.paramLabel}></span>
<span className={styles.paramValue}>{device.rated_power} kW</span>
</div>
)}
</div>
<div style={{ marginTop: 12 }}>
{params.map(param => {
const data = realtimeData[param.key];
const valueStr = data != null ? `${data.value}${param.unit ? ' ' + param.unit : ''}` : '--';
return (
<div key={param.key} className={styles.paramRow}>
<span className={styles.paramLabel}>{param.label}</span>
<span className={styles.paramValue}>{valueStr}</span>
</div>
);
})}
</div>
<button className={styles.detailBtn} onClick={() => onViewDetail(device)}>
</button>
</div>
);
}