diff --git a/VERSIONS.json b/VERSIONS.json
index 5ece7b5..54bbda9 100644
--- a/VERSIONS.json
+++ b/VERSIONS.json
@@ -1,9 +1,9 @@
{
"project": "zpark-ems",
- "project_version": "1.5.0",
+ "project_version": "1.6.0",
"customer": "Z-Park 中关村医疗器械园",
"core_version": "1.4.0",
"frontend_template_version": "1.4.0",
"last_updated": "2026-04-06",
- "notes": "Energy flow diagram, weather widget, curve templates, device comparison, dispersion analysis, PWA, alarm subscriptions"
+ "notes": "String monitoring, I-V diagnosis stub, ROI simulator, knowledge base, remote config stub, mobile responsive"
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 68a676b..43e4fe9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -23,8 +23,13 @@ import DataQuery from './pages/DataQuery';
import Management from './pages/Management';
import Prediction from './pages/Prediction';
import EnergyStrategy from './pages/EnergyStrategy';
+import ROISimulator from './pages/ROISimulator';
import AIOperations from './pages/AIOperations';
+import KnowledgeBase from './pages/KnowledgeBase';
import BigScreen from './pages/BigScreen';
+import StringMonitoring from './pages/StringMonitoring';
+import IVDiagnosis from './pages/IVDiagnosis';
+import RemoteConfig from './pages/RemoteConfig';
import { isLoggedIn } from './utils/auth';
@@ -66,7 +71,12 @@ function AppContent() {
} />
} />
} />
+ } />
} />
+ } />
+ } />
+ } />
+ } />
} />
diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx
index e0fe292..56cf2e0 100644
--- a/frontend/src/layouts/MainLayout.tsx
+++ b/frontend/src/layouts/MainLayout.tsx
@@ -7,7 +7,8 @@ import {
ThunderboltOutlined, AppstoreOutlined, WarningOutlined, CloseCircleOutlined,
InfoCircleOutlined, FundProjectionScreenOutlined, GlobalOutlined,
BulbOutlined, BulbFilled, FundOutlined, CarOutlined, ToolOutlined,
- SearchOutlined, SolutionOutlined, RobotOutlined, ExperimentOutlined,
+ SearchOutlined, SolutionOutlined, RobotOutlined, ExperimentOutlined, ApartmentOutlined, ScanOutlined,
+ DollarOutlined, BookOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -36,6 +37,15 @@ export default function MainLayout() {
const { darkMode, toggleDarkMode } = useTheme();
const { t, i18n } = useTranslation();
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth < 768) setCollapsed(true);
+ };
+ window.addEventListener('resize', handleResize);
+ handleResize();
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
useEffect(() => {
getBranding().then((res: any) => {
setFeatures(res?.features || {});
@@ -64,8 +74,17 @@ export default function MainLayout() {
{ key: '/data-query', icon: , label: t('menu.dataQuery', '数据查询') },
{ key: '/prediction', icon: , label: t('menu.prediction', 'AI预测') },
{ key: '/management', icon: , label: t('menu.management', '管理体系') },
+ { key: '/knowledge-base', icon: , label: t('menu.knowledgeBase', '知识库') },
{ key: '/energy-strategy', icon: , label: t('menu.energyStrategy', '策略优化') },
+ { key: '/roi-simulator', icon: , label: t('menu.roiSimulator', '投资回报') },
{ key: '/ai-operations', icon: , label: t('menu.aiOperations', 'AI运维') },
+ { key: '/string-monitoring', icon: , label: t('menu.stringMonitoring', '组串监控') },
+ { key: '/remote-config', icon: , label: t('menu.remoteConfig', '远程配置') },
+ { key: 'diagnosis-group', icon: , label: t('menu.diagnosis', '智能诊断'),
+ children: [
+ { key: '/iv-diagnosis', icon: , label: t('menu.ivDiagnosis', 'IV曲线诊断') },
+ ],
+ },
{ key: 'bigscreen-group', icon: , label: t('menu.bigscreen'),
children: [
{ key: '/bigscreen', icon: , label: t('menu.bigscreen2d') },
diff --git a/frontend/src/pages/IVDiagnosis/index.tsx b/frontend/src/pages/IVDiagnosis/index.tsx
new file mode 100644
index 0000000..aaa66ff
--- /dev/null
+++ b/frontend/src/pages/IVDiagnosis/index.tsx
@@ -0,0 +1,107 @@
+import { useMemo } from 'react';
+import { Alert, Card, Row, Col } from 'antd';
+import { ScanOutlined, BugOutlined, FileTextOutlined, LineChartOutlined } from '@ant-design/icons';
+import ReactECharts from 'echarts-for-react';
+
+const featureCards = [
+ { icon: , title: '在线IV扫描', desc: 'Remote I-V curve scanning at string level' },
+ { icon: , title: '23种故障诊断', desc: 'Automatic fault classification with 97% accuracy' },
+ { icon: , title: '诊断报告', desc: 'Auto-generated diagnosis reports with repair recommendations' },
+ { icon: , title: '趋势分析', desc: 'I-V curve degradation tracking over time' },
+];
+
+// Generate a typical I-V curve: I = Isc * (1 - exp((V - Voc) / Vt))
+const generateIVData = () => {
+ const Isc = 10.5;
+ const Voc = 38;
+ const Vt = 3.2;
+ const points: { v: number; i: number; p: number }[] = [];
+ for (let v = 0; v <= Voc; v += 0.5) {
+ const i = Math.max(0, Isc * (1 - Math.exp((v - Voc) / Vt)));
+ points.push({ v, i, p: v * i });
+ }
+ return points;
+};
+
+export default function IVDiagnosis() {
+ const ivData = useMemo(() => generateIVData(), []);
+
+ const chartOption = {
+ tooltip: { trigger: 'axis' as const },
+ legend: { data: ['电流 (A)', '功率 (W)'] },
+ xAxis: { type: 'value' as const, name: '电压 (V)', min: 0, max: 40 },
+ yAxis: [
+ { type: 'value' as const, name: '电流 (A)', min: 0, max: 12 },
+ { type: 'value' as const, name: '功率 (W)', min: 0, max: 400 },
+ ],
+ series: [
+ {
+ name: '电流 (A)',
+ type: 'line',
+ smooth: true,
+ data: ivData.map(p => [p.v, +p.i.toFixed(2)]),
+ lineStyle: { width: 2.5, color: '#1890ff' },
+ itemStyle: { color: '#1890ff' },
+ symbol: 'none',
+ },
+ {
+ name: '功率 (W)',
+ type: 'line',
+ smooth: true,
+ yAxisIndex: 1,
+ data: ivData.map(p => [p.v, +p.p.toFixed(1)]),
+ lineStyle: { width: 2.5, color: '#fa541c' },
+ itemStyle: { color: '#fa541c' },
+ symbol: 'none',
+ areaStyle: { color: 'rgba(250,84,28,0.08)' },
+ },
+ ],
+ graphic: [
+ {
+ type: 'text',
+ left: '15%',
+ top: '15%',
+ style: { text: 'Isc = 10.5A', fontSize: 12, fill: '#1890ff' },
+ },
+ {
+ type: 'text',
+ right: '18%',
+ bottom: '22%',
+ style: { text: 'Voc = 38V', fontSize: 12, fill: '#1890ff' },
+ },
+ {
+ type: 'text',
+ left: 'center',
+ top: '12%',
+ style: { text: 'Pmax = 298W @ 30.5V', fontSize: 12, fill: '#fa541c', fontWeight: 'bold' },
+ },
+ ],
+ };
+
+ return (
+
+
+
+
+ {featureCards.map(card => (
+
+
+ {card.icon}
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/KnowledgeBase/index.tsx b/frontend/src/pages/KnowledgeBase/index.tsx
new file mode 100644
index 0000000..c7c3c8a
--- /dev/null
+++ b/frontend/src/pages/KnowledgeBase/index.tsx
@@ -0,0 +1,252 @@
+import { useState, useMemo } from 'react';
+import { Card, Input, Tabs, List, Tag, Typography, Empty } from 'antd';
+import {
+ BookOutlined, SearchOutlined, ToolOutlined, WarningOutlined,
+ SafetyOutlined, FileTextOutlined,
+} from '@ant-design/icons';
+
+const { Title, Paragraph, Text } = Typography;
+
+interface Article {
+ id: number;
+ category: string;
+ title: string;
+ date: string;
+ content: string;
+}
+
+const CATEGORY_MAP: Record = {
+ maintenance: { label: '设备维护', color: 'blue', icon: },
+ troubleshooting: { label: '故障排查', color: 'orange', icon: },
+ safety: { label: '安全规范', color: 'red', icon: },
+ operation: { label: '操作手册', color: 'green', icon: },
+};
+
+const articles: Article[] = [
+ {
+ id: 1,
+ category: 'maintenance',
+ title: '逆变器日常巡检规程',
+ date: '2026-04-01',
+ content: `## 巡检频率
+每周一次
+
+## 巡检项目
+1. 检查逆变器运行指示灯状态
+2. 检查散热风扇运转是否正常
+3. 检查接线端子是否松动
+4. 记录当前功率和日发电量
+5. 检查通信模块信号强度
+
+## 注意事项
+- 巡检时不得打开逆变器外壳
+- 异常情况及时上报运维主管`,
+ },
+ {
+ id: 2,
+ category: 'troubleshooting',
+ title: '逆变器常见故障代码及处理',
+ date: '2026-04-01',
+ content: `## 常见故障代码
+| 代码 | 含义 | 处理方法 |
+|------|------|----------|
+| F01 | 电网电压过高 | 检查电网侧电压,联系供电部门 |
+| F02 | 电网频率异常 | 等待电网恢复,自动并网 |
+| F03 | 直流过压 | 检查组串接线,确认组串电压 |
+| F04 | 绝缘阻抗低 | 检查组件和线缆绝缘 |
+| F05 | 温度过高 | 清洁散热器,检查环境温度 |`,
+ },
+ {
+ id: 3,
+ category: 'safety',
+ title: '光伏电站安全操作规范',
+ date: '2026-04-01',
+ content: `## 基本安全要求
+1. 所有操作必须两人以上配合
+2. 必须穿戴绝缘手套和安全帽
+3. 雷雨天气禁止室外操作
+4. 操作前确认设备已断电
+5. 使用万用表确认无残余电压`,
+ },
+ {
+ id: 4,
+ category: 'operation',
+ title: 'iSolarCloud 数据导出操作指南',
+ date: '2026-04-01',
+ content: `## 导出步骤
+1. 登录 iSolarCloud 平台
+2. 进入电站详情 → 曲线
+3. 选择时间范围和参数
+4. 点击右上角导出按钮
+5. 选择 Excel 格式下载`,
+ },
+ {
+ id: 5,
+ category: 'maintenance',
+ title: '光伏组件清洗规程',
+ date: '2026-04-01',
+ content: `## 清洗频率
+每季度一次(春季花粉期加密)
+
+## 清洗方法
+1. 使用纯净水或去离子水
+2. 软质刷子或海绵擦拭
+3. 从上到下顺序冲洗
+4. 避免使用化学清洁剂
+
+## 最佳时间
+清晨或傍晚,避免组件高温时清洗`,
+ },
+];
+
+function renderMarkdown(text: string) {
+ const lines = text.trim().split('\n');
+ const elements: React.ReactNode[] = [];
+ let tableRows: string[][] = [];
+ let inTable = false;
+
+ const flushTable = () => {
+ if (tableRows.length === 0) return;
+ const header = tableRows[0];
+ const body = tableRows.slice(1);
+ elements.push(
+
+
+
+ {header.map((h, i) => (
+ | {h} |
+ ))}
+
+
+
+ {body.map((row, ri) => (
+
+ {row.map((cell, ci) => (
+ | {cell} |
+ ))}
+
+ ))}
+
+
+ );
+ tableRows = [];
+ inTable = false;
+ };
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ if (line.startsWith('|') && line.endsWith('|')) {
+ const cells = line.split('|').slice(1, -1).map(c => c.trim());
+ if (cells.every(c => /^[-:]+$/.test(c))) continue; // separator
+ tableRows.push(cells);
+ inTable = true;
+ continue;
+ }
+ if (inTable) flushTable();
+
+ if (line.startsWith('## ')) {
+ elements.push({line.slice(3)});
+ } else if (/^\d+\.\s/.test(line)) {
+ elements.push({line});
+ } else if (line.startsWith('- ')) {
+ elements.push({line});
+ } else if (line.trim() === '') {
+ // skip
+ } else {
+ elements.push({line});
+ }
+ }
+ if (inTable) flushTable();
+
+ return <>{elements}>;
+}
+
+export default function KnowledgeBase() {
+ const [search, setSearch] = useState('');
+ const [activeTab, setActiveTab] = useState('all');
+ const [expandedId, setExpandedId] = useState(null);
+
+ const filtered = useMemo(() => {
+ return articles.filter(a => {
+ const matchCategory = activeTab === 'all' || a.category === activeTab;
+ const matchSearch = !search || a.title.includes(search) || a.content.includes(search);
+ return matchCategory && matchSearch;
+ });
+ }, [search, activeTab]);
+
+ const tabItems = [
+ { key: 'all', label: '全部' },
+ ...Object.entries(CATEGORY_MAP).map(([key, val]) => ({
+ key,
+ label: {val.icon} {val.label},
+ })),
+ ];
+
+ return (
+
+
+
+ 运维知识库
+
+
+
+ }
+ allowClear
+ value={search}
+ onChange={e => setSearch(e.target.value)}
+ size="large"
+ />
+
+
+
+
+ {filtered.length === 0 ? (
+
+ ) : (
+
{
+ const cat = CATEGORY_MAP[article.category];
+ const isExpanded = expandedId === article.id;
+ const preview = article.content.split('\n').filter(l => l.trim() && !l.startsWith('#')).slice(0, 2).join(' ');
+ return (
+
+ setExpandedId(isExpanded ? null : article.id)}
+ style={{ cursor: 'pointer' }}
+ title={
+
+ {cat?.label}
+ {article.title}
+
+ }
+ extra={{article.date}}
+ >
+ {isExpanded ? (
+
+ {renderMarkdown(article.content)}
+
+ ) : (
+
+ {preview.length > 80 ? preview.slice(0, 80) + '...' : preview}
+
+ )}
+
+
+ );
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/ROISimulator/index.tsx b/frontend/src/pages/ROISimulator/index.tsx
new file mode 100644
index 0000000..b2170f1
--- /dev/null
+++ b/frontend/src/pages/ROISimulator/index.tsx
@@ -0,0 +1,320 @@
+import { useState, useMemo } from 'react';
+import { Card, Form, InputNumber, Button, Row, Col, Statistic, Table, Typography, Divider } from 'antd';
+import { DollarOutlined, CalculatorOutlined, ReloadOutlined } from '@ant-design/icons';
+import ReactEChartsCore from 'echarts-for-react/lib/core';
+import * as echarts from 'echarts/core';
+import { LineChart, BarChart } from 'echarts/charts';
+import { GridComponent, TooltipComponent, LegendComponent, MarkLineComponent } from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+
+echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, MarkLineComponent, CanvasRenderer]);
+
+const { Title } = Typography;
+
+interface SimParams {
+ capacity: number;
+ costPerWp: number;
+ degradationRate: number;
+ sellPrice: number;
+ selfConsumption: number;
+ feedInTariff: number;
+ omRate: number;
+ period: number;
+ peakHours: number;
+ sunDays: number;
+}
+
+interface YearData {
+ year: number;
+ generation: number;
+ revenue: number;
+ omCost: number;
+ netIncome: number;
+ cumulative: number;
+}
+
+interface SimResult {
+ totalInvestment: number;
+ paybackYear: number | null;
+ irr: number;
+ npv: number;
+ years: YearData[];
+}
+
+const defaultParams: SimParams = {
+ capacity: 2710,
+ costPerWp: 3.5,
+ degradationRate: 0.5,
+ sellPrice: 0.65,
+ selfConsumption: 80,
+ feedInTariff: 0.35,
+ omRate: 1.0,
+ period: 25,
+ peakHours: 4.5,
+ sunDays: 260,
+};
+
+function calculateIRR(cashFlows: number[]): number {
+ let rate = 0.1;
+ for (let i = 0; i < 100; i++) {
+ const npv = cashFlows.reduce((sum, cf, t) => sum + cf / Math.pow(1 + rate, t), 0);
+ const dnpv = cashFlows.reduce((sum, cf, t) => sum - t * cf / Math.pow(1 + rate, t + 1), 0);
+ if (Math.abs(dnpv) < 1e-10) break;
+ const newRate = rate - npv / dnpv;
+ if (Math.abs(newRate - rate) < 1e-8) break;
+ rate = newRate;
+ }
+ return rate;
+}
+
+function calculateNPV(cashFlows: number[], discountRate: number): number {
+ return cashFlows.reduce((sum, cf, t) => sum + cf / Math.pow(1 + discountRate, t), 0);
+}
+
+function simulate(params: SimParams): SimResult {
+ const totalInvestment = params.capacity * params.costPerWp;
+ const years: YearData[] = [];
+ let cumulative = -totalInvestment;
+ let paybackYear: number | null = null;
+
+ for (let y = 1; y <= params.period; y++) {
+ const degradation = Math.pow(1 - params.degradationRate / 100, y - 1);
+ const annualGen = params.capacity * params.peakHours * params.sunDays * degradation;
+ const selfConsumed = annualGen * params.selfConsumption / 100;
+ const exported = annualGen - selfConsumed;
+ const revenue = selfConsumed * params.sellPrice + exported * params.feedInTariff;
+ const omCost = totalInvestment * params.omRate / 100;
+ const netIncome = revenue - omCost;
+ cumulative += netIncome;
+
+ if (cumulative >= 0 && paybackYear === null) paybackYear = y;
+
+ years.push({ year: y, generation: annualGen, revenue, omCost, netIncome, cumulative });
+ }
+
+ const cashFlows = [-totalInvestment, ...years.map(y => y.netIncome)];
+ const irr = calculateIRR(cashFlows);
+ const npv = calculateNPV(cashFlows, 0.06);
+
+ return { totalInvestment, paybackYear, irr, npv, years };
+}
+
+export default function ROISimulator() {
+ const [form] = Form.useForm();
+ const [params, setParams] = useState(defaultParams);
+
+ const result = useMemo(() => simulate(params), [params]);
+
+ const handleCalculate = () => {
+ form.validateFields().then(values => {
+ setParams(values as SimParams);
+ });
+ };
+
+ const handleReset = () => {
+ form.setFieldsValue(defaultParams);
+ setParams(defaultParams);
+ };
+
+ const chartOption = useMemo(() => ({
+ tooltip: {
+ trigger: 'axis',
+ formatter: (p: any) => {
+ const items = p.map((s: any) =>
+ `${s.marker} ${s.seriesName}: ${(s.value / 10000).toFixed(2)} 万元`
+ );
+ return `第 ${p[0].axisValue} 年
${items.join('
')}`;
+ },
+ },
+ legend: { data: ['年净收益', '累计现金流'] },
+ grid: { left: 60, right: 30, bottom: 30, top: 40 },
+ xAxis: {
+ type: 'category',
+ data: result.years.map(y => y.year),
+ name: '年',
+ },
+ yAxis: {
+ type: 'value',
+ name: '万元',
+ axisLabel: { formatter: (v: number) => (v / 10000).toFixed(0) },
+ },
+ series: [
+ {
+ name: '年净收益',
+ type: 'bar',
+ data: result.years.map(y => y.netIncome),
+ itemStyle: { color: '#52c41a' },
+ barMaxWidth: 20,
+ },
+ {
+ name: '累计现金流',
+ type: 'line',
+ data: result.years.map(y => y.cumulative),
+ itemStyle: { color: '#1890ff' },
+ lineStyle: { width: 2 },
+ markLine: {
+ silent: true,
+ data: [{ yAxis: 0, lineStyle: { color: '#ff4d4f', type: 'dashed' } }],
+ },
+ },
+ ],
+ }), [result]);
+
+ const columns = [
+ { title: '年份', dataIndex: 'year', key: 'year', width: 70 },
+ {
+ title: '发电量 (kWh)', dataIndex: 'generation', key: 'generation',
+ render: (v: number) => v.toLocaleString('zh-CN', { maximumFractionDigits: 0 }),
+ },
+ {
+ title: '收入 (元)', dataIndex: 'revenue', key: 'revenue',
+ render: (v: number) => v.toLocaleString('zh-CN', { maximumFractionDigits: 0 }),
+ },
+ {
+ title: '运维成本 (元)', dataIndex: 'omCost', key: 'omCost',
+ render: (v: number) => v.toLocaleString('zh-CN', { maximumFractionDigits: 0 }),
+ },
+ {
+ title: '净收益 (元)', dataIndex: 'netIncome', key: 'netIncome',
+ render: (v: number) => = 0 ? '#52c41a' : '#ff4d4f' }}>
+ {v.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}
+ ,
+ },
+ {
+ title: '累计 (元)', dataIndex: 'cumulative', key: 'cumulative',
+ render: (v: number) => = 0 ? '#52c41a' : '#ff4d4f' }}>
+ {v.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}
+ ,
+ },
+ ];
+
+ return (
+
+
+
+ 光伏投资回报模拟器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } block onClick={handleCalculate}>
+ 计算
+
+
+
+ } block onClick={handleReset}>
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ 0.08 ? '#52c41a' : '#faad14' }}
+ />
+
+
+
+
+ 0 ? '#52c41a' : '#ff4d4f' }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/RemoteConfig/index.tsx b/frontend/src/pages/RemoteConfig/index.tsx
new file mode 100644
index 0000000..5a24aac
--- /dev/null
+++ b/frontend/src/pages/RemoteConfig/index.tsx
@@ -0,0 +1,104 @@
+import { useEffect, useState } from 'react';
+import { Alert, Select, Card, Descriptions, Button, Tooltip, Timeline, Typography, Space, Row, Col } from 'antd';
+import { LockOutlined, SettingOutlined, ReadOutlined, UploadOutlined, ReloadOutlined, RocketOutlined } from '@ant-design/icons';
+import { getDevices } from '../../services/api';
+
+const { Title, Text } = Typography;
+
+const SAMPLE_PARAMS = [
+ { label: '并网电压范围', value: '185V - 265V' },
+ { label: '并网频率范围', value: '47.5Hz - 51.5Hz' },
+ { label: '功率因数', value: '1.0' },
+ { label: '无功功率模式', value: 'Disabled' },
+ { label: '孤岛保护', value: 'Enabled' },
+ { label: 'MPPT范围', value: '200V - 850V' },
+ { label: '最大交流电流', value: '63A' },
+];
+
+export default function RemoteConfig() {
+ const [devices, setDevices] = useState([]);
+ const [selectedDevice, setSelectedDevice] = useState();
+
+ useEffect(() => {
+ getDevices({ device_type: 'inverter' }).then((res: any) => {
+ const list = Array.isArray(res) ? res : res?.items || [];
+ setDevices(list);
+ }).catch(() => {});
+ }, []);
+
+ return (
+
+
+
+ 远程配置
+
+
+
+
+
+
+
+
+
+ 参数模板预览>} size="small">
+
+ {SAMPLE_PARAMS.map((p) => (
+ {p.label}>}
+ >
+ {p.value}
+
+ ))}
+
+
+
+
+ } disabled>读取参数
+
+
+ } disabled>下发参数
+
+
+ } disabled>固件升级
+
+
+ } disabled>重启设备
+
+
+
+
+
+
+
+ v1.6 — UI Preview (当前)> },
+ { color: 'blue', children: <>v2.0 — 只读参数访问> },
+ { color: 'blue', children: <>v2.1 — 参数下发(含安全检查)> },
+ { color: 'gray', children: <>v2.2 — 固件 OTA 升级> },
+ ]}
+ />
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/StringMonitoring/index.tsx b/frontend/src/pages/StringMonitoring/index.tsx
new file mode 100644
index 0000000..45d533c
--- /dev/null
+++ b/frontend/src/pages/StringMonitoring/index.tsx
@@ -0,0 +1,146 @@
+import { useState, useMemo } from 'react';
+import { Card, Select, Table, Tag, Alert, Row, Col } from 'antd';
+import { ApartmentOutlined } from '@ant-design/icons';
+import ReactECharts from 'echarts-for-react';
+
+const INVERTERS = [
+ 'AP101', 'AP102', 'AP103', 'AP104', 'AP105', 'AP106', 'AP107', 'AP108',
+ 'AP201', 'AP202', 'AP203', 'AP204', 'AP205', 'AP206', 'AP207', 'AP208',
+];
+
+interface StringData {
+ id: string;
+ voltage: number;
+ current: number;
+ power: number;
+ status: 'normal' | 'low' | 'alarm';
+}
+
+const generateStringData = (inverterId: string): StringData[] => {
+ const count = inverterId.startsWith('AP1') ? 8 : 16;
+ return Array.from({ length: count }, (_, i) => {
+ const voltage = 320 + Math.random() * 80;
+ const current = 8 + Math.random() * 4;
+ const isLow = Math.random() < 0.1;
+ const isAlarm = Math.random() < 0.05;
+ const v = isAlarm ? voltage * 0.3 : isLow ? voltage * 0.7 : voltage;
+ const c = isAlarm ? current * 0.2 : isLow ? current * 0.6 : current;
+ return {
+ id: `STR-${String(i + 1).padStart(2, '0')}`,
+ voltage: v,
+ current: c,
+ power: v * c,
+ status: isAlarm ? 'alarm' : isLow ? 'low' : 'normal',
+ };
+ });
+};
+
+const statusMap: Record = {
+ normal: { color: 'green', text: '正常' },
+ low: { color: 'orange', text: '低效' },
+ alarm: { color: 'red', text: '告警' },
+};
+
+const heatColor = (status: string, current: number, mean: number) => {
+ if (status === 'alarm') return '#ff4d4f';
+ if (status === 'low') return '#faad14';
+ if (current >= mean) return '#52c41a';
+ return '#95de64';
+};
+
+export default function StringMonitoring() {
+ const [inverterId, setInverterId] = useState(INVERTERS[0]);
+
+ const data = useMemo(() => generateStringData(inverterId), [inverterId]);
+ const meanCurrent = useMemo(() => data.reduce((s, d) => s + d.current, 0) / data.length, [data]);
+
+ const columns = [
+ { title: '组串ID', dataIndex: 'id', key: 'id' },
+ { title: '电压 (V)', dataIndex: 'voltage', key: 'voltage', render: (v: number) => v.toFixed(1) },
+ { title: '电流 (A)', dataIndex: 'current', key: 'current', render: (v: number) => v.toFixed(2) },
+ { title: '功率 (W)', dataIndex: 'power', key: 'power', render: (v: number) => v.toFixed(0) },
+ { title: '状态', dataIndex: 'status', key: 'status', render: (s: string) => {
+ const st = statusMap[s] || { color: 'default', text: s };
+ return {st.text};
+ }},
+ ];
+
+ const barOption = {
+ tooltip: { trigger: 'axis' as const },
+ xAxis: { type: 'category' as const, data: data.map(d => d.id) },
+ yAxis: { type: 'value' as const, name: '电流 (A)' },
+ series: [
+ {
+ type: 'bar',
+ data: data.map(d => ({
+ value: +d.current.toFixed(2),
+ itemStyle: { color: statusMap[d.status]?.color === 'green' ? '#52c41a' : statusMap[d.status]?.color === 'orange' ? '#faad14' : '#ff4d4f' },
+ })),
+ barMaxWidth: 40,
+ },
+ {
+ type: 'line',
+ markLine: {
+ silent: true,
+ symbol: 'none',
+ data: [{ yAxis: +meanCurrent.toFixed(2), label: { formatter: `均值 ${meanCurrent.toFixed(2)}A` } }],
+ lineStyle: { color: '#1890ff', type: 'dashed' as const },
+ },
+ data: [],
+ },
+ ],
+ };
+
+ // Heatmap grid
+ const gridCols = data.length <= 8 ? 4 : 4;
+ const gridRows = Math.ceil(data.length / gridCols);
+
+ return (
+
+
+
+
组串监控>} extra={
+
+
+
+
+
+
+
+
+
+
+
+ {data.map(d => (
+
+
{d.id}
+
{d.current.toFixed(1)}A / {d.power.toFixed(0)}W
+
+ ))}
+
+
+ 正常
+ 低效
+ 告警
+
+
+
+
+
+ );
+}