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>
179 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|