告警中心 初始化

This commit is contained in:
mmc
2026-05-07 14:13:26 +08:00
parent e401eef62f
commit 9cf26ce2bd
19 changed files with 1321 additions and 928 deletions

32
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"axios": "^1.15.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"echarts": "^6.0.0",
"express": "^4.21.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.546.0",
@@ -3515,6 +3516,22 @@
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7104,6 +7121,21 @@
"engines": {
"node": ">=6"
}
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
}
}

View File

@@ -20,6 +20,7 @@
"axios": "^1.15.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"echarts": "^6.0.0",
"express": "^4.21.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.546.0",

18
src/api/deviceAccess.ts Normal file
View File

@@ -0,0 +1,18 @@
import request from '../request';
//新增物模型
export function addDeviceModel(data) {
return request({
url: 'system/devicemodel/add',
method: 'post',
data
});
}

View File

@@ -0,0 +1,349 @@
import { useState } from 'react';
import {
Card, Table, Button, Space, Form, Modal, Input, Select, InputNumber,
Tabs, Tag, Popconfirm, message, Divider, Switch, Empty
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined, ProductOutlined,
AppstoreOutlined, SwapOutlined, VideoCameraOutlined, AlertOutlined
} from '@ant-design/icons';
const { Option } = Select;
const { TabPane } = Tabs;
// ====================== 模拟基础数据 ======================
const productList = [
{
id: 1, name: '海康威视摄像头', code: 'hikvision', protocolType: 'onvif', manufacturer: '海康',
publishTopic: '/hik/device/upload', subscribeTopic: '/hik/device/cmd'
},
{
id: 2, name: '大华摄像头', code: 'dahua', protocolType: 'rtsp', manufacturer: '大华',
publishTopic: '/dahua/device/upload', subscribeTopic: '/dahua/device/cmd'
},
{
id: 3, name: '通用MQTT设备', code: 'mqtt_device', protocolType: 'mqtt', manufacturer: '通用',
publishTopic: '/iot/device/upload', subscribeTopic: '/iot/device/cmd'
},
];
const modelList = [
{ id: 10, name: '通用视频监控设备', code: 'video_camera', desc: '摄像头、云台、告警' },
{ id: 11, name: '环境监测设备', code: 'env_sensor', desc: '温湿度、烟感' },
{ id: 12, name: '电池供电设备', code: 'battery_device', desc: '电量、电压监测' },
];
const modelDetailList: Record<number, {
props: any[];
events: any[];
actions: any[];
}> = {
10: {
props: [
{ id: 1, code: 'device_online', name: '在线状态', type: 'bool', rw: 'r', isAlarm: false },
{ id: 2, code: 'record_status', name: '录像状态', type: 'string', rw: 'r', isAlarm: false },
],
events: [{ id: 1, code: 'alarm_perimeter', name: '周界入侵告警' }],
actions: [{ id: 1, code: 'take_snapshot', name: '截图' }]
},
11: {
props: [
{ id: 1, code: 'temp', name: '温度', type: 'float', rw: 'r', isAlarm: true },
{ id: 2, code: 'humi', name: '湿度', type: 'float', rw: 'r', isAlarm: true },
],
events: [{ id: 1, code: 'temp_over', name: '超温告警' }],
actions: []
},
12: {
props: [
{ id: 1, code: 'battery', name: '电量', type: 'int', rw: 'r', isAlarm: true },
{ id: 2, code: 'voltage', name: '电压', type: 'float', rw: 'r', isAlarm: true },
],
events: [{ id: 1, code: 'low_power', name: '低电量告警' }],
actions: []
}
};
let mappingInitList = [
{ id: 1, productId: 1, standardCode: 'device_online', sourceField: 'online_flag', convertRule: 'value==1?true:false' }
];
const deviceList = [
{
id: 1, name: '1#阵列区球机', sn: 'HK20260507001',
productId: 1, productName: '海康威视摄像头', modelId: 10, modelName: '通用视频监控设备',
protocolType: 'onvif', ip: '192.168.1.65', port: 80, username: 'admin', password: '123456',
mqttServer: '', mqttPort: 1883, clientId: '', mqttUsername: '', mqttPassword: '',
alarmConfig: {}, onlineStatus: 1, createTime: '2026-05-07 10:23:45'
},
{
id: 2, name: '1#环境传感器', sn: 'SEN001',
productId: 3, productName: '通用MQTT设备', modelId: 11, modelName: '环境监测设备',
protocolType: 'mqtt', mqttServer: 'tcp://192.168.1.200', mqttPort: 1883, clientId: 'sen_001',
alarmConfig: { tempMax: 60, tempMin: 0, humiMax: 90, humiMin: 20 }, onlineStatus: 1
}
];
const alarmList = [{ id: 1, deviceName: '1#环境传感器', level: 'warn', content: '温度超过阈值 62℃', time: '2026-05-07 10:24:00' }];
export default function DeviceAccessPage() {
const [activeTab, setActiveTab] = useState('device');
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [modalType, setModalType] = useState('');
const [editData, setEditData] = useState<any>(null);
const [deviceProtocol, setDeviceProtocol] = useState('onvif');
const [productProtocol, setProductProtocol] = useState('');
const [modelDetail, setModelDetail] = useState<any>({ props: [], events: [], actions: [] });
const [itemModal, setItemModal] = useState(false);
const [itemType, setItemType] = useState<'props' | 'events' | 'actions'>('props');
const [itemForm] = Form.useForm();
// 字段映射(实时增删改查,实时保存)
const [mappingList, setMappingList] = useState(mappingInitList);
const [mappingModal, setMappingModal] = useState(false);
const [mappingForm] = Form.useForm();
// 打开弹窗
const openModal = (type: string, record?: any) => {
setModalType(type);
setEditData(record);
form.resetFields();
if (record) {
form.setFieldsValue(record);
if (type === 'model') setModelDetail(modelDetailList[record.id] || { props: [], events: [], actions: [] });
if (type === 'device') {
setDeviceProtocol(record.protocolType);
setModelDetail(modelDetailList[record.modelId] || { props: [] });
}
if (type === 'product') setProductProtocol(record.protocolType || '');
} else {
if (type === 'device') { setDeviceProtocol('onvif'); setModelDetail({ props: [] }); }
else if (type === 'product') setProductProtocol('');
else setModelDetail({ props: [], events: [], actions: [] });
}
setVisible(true);
};
// 产品提交
const handleSubmit = () => {
form.validateFields().then(values => {
message.success('产品信息保存成功');
setVisible(false);
});
};
// 物模型
const openItemModal = (type: 'props' | 'events' | 'actions', record?: any) => {
setItemType(type);
itemForm.resetFields();
if (record) itemForm.setFieldsValue(record);
setItemModal(true);
};
const saveItem = () => {
itemForm.validateFields().then((val) => {
const list = [...modelDetail[itemType]];
val.id ? list.splice(list.findIndex(x => x.id === val.id), 1, val) : list.push({ ...val, id: Date.now() });
setModelDetail({ ...modelDetail, [itemType]: list });
setItemModal(false);
});
};
const deleteItem = (type: 'props' | 'events' | 'actions', id: number) => {
const list = modelDetail[type].filter(x => x.id !== id);
setModelDetail({ ...modelDetail, [type]: list });
};
// ===================== 字段映射:增删改 立即保存 =====================
const openMappingModal = (record?: any) => {
mappingForm.resetFields();
if (record) mappingForm.setFieldsValue(record);
else mappingForm.setFieldsValue({ productId: editData?.id });
setMappingModal(true);
};
// 新增/编辑 → 立即保存
const saveMapping = () => {
mappingForm.validateFields().then(val => {
let newList = [...mappingList];
if (val.id) {
// 编辑
const index = newList.findIndex(x => x.id === val.id);
if (index >= 0) newList[index] = val;
message.success('字段映射修改成功');
} else {
// 新增
newList.push({ ...val, id: Date.now() });
message.success('字段映射新增成功');
}
setMappingList(newList);
mappingInitList = newList;
setMappingModal(false);
});
};
// 删除 → 立即保存
const deleteMapping = (id: number) => {
const newList = mappingList.filter(x => x.id !== id);
setMappingList(newList);
mappingInitList = newList;
message.success('字段映射删除成功');
};
// 获取告警属性
const getAlarmProps = () => modelDetail?.props?.filter(item => item.isAlarm) || [];
// 表格列
const deviceColumns = [
{ title: '设备名称', dataIndex: 'name' },
{ title: 'SN', dataIndex: 'sn' },
{ title: '所属产品', dataIndex: 'productName' },
{ title: '物模型', dataIndex: 'modelName' },
{ title: '协议', dataIndex: 'protocolType', render: t => <Tag>{t.toUpperCase()}</Tag> },
{ title: '状态', render: r => <Tag color={r.onlineStatus ? 'green' : 'red'}>{r.onlineStatus ? '在线' : '离线'}</Tag> },
{ title: '操作', render: r => <Space><Button type="text" icon={<EditOutlined />} onClick={() => openModal('device', r)}>编辑</Button></Space> }
];
const productColumns = [
{ title: '产品名称', dataIndex: 'name' },
{ title: '厂商', dataIndex: 'manufacturer' },
{ title: '协议', dataIndex: 'protocolType' },
{ title: '上报Topic', dataIndex: 'publishTopic' },
{ title: '订阅Topic', dataIndex: 'subscribeTopic' },
{
title: '操作', render: r => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('product', r)}>编辑</Button>
<Button type="text" icon={<SwapOutlined />} onClick={() => openModal('product', r)}>字段映射</Button>
</Space>
)
}
];
const modelColumns = [
{ title: '模型名称', dataIndex: 'name' },
{ title: '描述', dataIndex: 'desc' },
{ title: '操作', render: r => <Button type="text" icon={<EditOutlined />} onClick={() => openModal('model', r)}>编辑</Button> }
];
return (
<div className="p-5 bg-[#f5f7fa] min-h-screen">
<Card className="mb-4">
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={<span><VideoCameraOutlined />设备管理</span>} key="device" />
<TabPane tab={<span><ProductOutlined />产品管理</span>} key="product" />
<TabPane tab={<span><AppstoreOutlined />物模型管理</span>} key="model" />
</Tabs>
</Card>
{activeTab === 'device' && <Card><Space className="mb-3"><Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('device')}>新增设备</Button></Space><Table rowKey="id" columns={deviceColumns} dataSource={deviceList} /></Card>}
{activeTab === 'product' && <Card><Space><Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('product')}>新增产品</Button></Space><Table rowKey="id" columns={productColumns} dataSource={productList} /></Card>}
{activeTab === 'model' && <Card><Space><Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('model')}>新增物模型</Button></Space><Table rowKey="id" columns={modelColumns} dataSource={modelList} /></Card>}
<Modal open={visible} width={850} onCancel={() => setVisible(false)} onOk={handleSubmit} destroyOnClose
title={modalType === 'device' ? '设备配置' : modalType === 'product' ? '产品配置' : '物模型配置'}>
<Form form={form} layout="vertical">
{modalType === 'device' && (
<Tabs defaultActiveKey="base">
<TabPane tab="基础信息" key="base">
<Form.Item label="设备名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="设备SN" name="sn" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="关联产品" name="productId" rules={[{ required: true }]}><Select>{productList.map(p => <Option key={p.id} value={p.id}>{p.name}</Option>)}</Select></Form.Item>
<Form.Item label="关联物模型" name="modelId" rules={[{ required: true }]}><Select onChange={val => setModelDetail(modelDetailList[val] || { props: [] })}>{modelList.map(m => <Option key={m.id} value={m.id}>{m.name}</Option>)}</Select></Form.Item>
</TabPane>
<TabPane tab="协议配置" key="protocol">
<Form.Item label="设备协议类型" name="protocolType" rules={[{ required: true }]}>
<Select value={deviceProtocol} onChange={setDeviceProtocol}>
<Option value="onvif">ONVIF 协议</Option>
<Option value="rtsp">RTSP 视频流</Option>
<Option value="mqtt">MQTT 物联网协议</Option>
</Select>
</Form.Item>
<Divider />
{deviceProtocol === 'onvif' && <><Form.Item label="设备IP地址" name="ip"><Input /></Form.Item><Form.Item label="访问端口" name="port"><InputNumber className="w-full" /></Form.Item><Form.Item label="登录账号" name="username"><Input /></Form.Item><Form.Item label="登录密码" name="password"><Input.Password /></Form.Item></>}
{deviceProtocol === 'rtsp' && <Form.Item label="RTSP 完整流地址" name="rtspUrl"><Input /></Form.Item>}
{deviceProtocol === 'mqtt' && <><Form.Item label="MQTT 服务地址" name="mqttServer"><Input /></Form.Item><Form.Item label="MQTT 端口" name="mqttPort"><InputNumber defaultValue={1883} /></Form.Item><Form.Item label="设备 ClientID" name="clientId"><Input /></Form.Item><Form.Item label="MQTT 用户名" name="mqttUsername"><Input /></Form.Item><Form.Item label="MQTT 密码" name="mqttPassword"><Input.Password /></Form.Item></>}
</TabPane>
<TabPane tab="报警阈值" key="alarm">
{getAlarmProps().map(item => <div key={item.code}><Form.Item label={`${item.name} 上限`} name={['alarmConfig', `${item.code}Max`]}><InputNumber className="w-full" /></Form.Item><Form.Item label={`${item.name} 下限`} name={['alarmConfig', `${item.code}Min`]}><InputNumber className="w-full" /></Form.Item></div>)}
{getAlarmProps().length === 0 && <Empty description="当前物模型无配置告警参数" />}
</TabPane>
</Tabs>
)}
{modalType === 'product' && (
<Tabs defaultActiveKey="base">
<TabPane tab="基础信息" key="base">
<Form.Item label="产品名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="厂商名称" name="manufacturer"><Input /></Form.Item>
<Form.Item label="默认协议类型" name="protocolType"><Select value={productProtocol} onChange={setProductProtocol}><Option value="onvif">ONVIF</Option><Option value="rtsp">RTSP</Option><Option value="mqtt">MQTT</Option></Select></Form.Item>
{productProtocol === 'mqtt' && <><Divider>MQTT 厂商级 Topic 配置</Divider><Form.Item label="设备上报 Topic" name="publishTopic"><Input /></Form.Item><Form.Item label="平台下发 Topic" name="subscribeTopic"><Input /></Form.Item></>}
</TabPane>
<TabPane tab="字段映射" key="mapping">
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openMappingModal()} className="mb-2">新增映射</Button>
<Table size="small" dataSource={mappingList.filter(x => x.productId === editData?.id)} pagination={false}
columns={[
{ title: '标准字段', dataIndex: 'standardCode' },
{ title: '厂商原始字段', dataIndex: 'sourceField' },
{ title: '转换规则', dataIndex: 'convertRule' },
{
title: '操作', render: r => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openMappingModal(r)}>编辑</Button>
<Popconfirm onConfirm={() => deleteMapping(r.id)} title="确定删除?"><Button type="text" danger icon={<DeleteOutlined />}>删除</Button></Popconfirm>
</Space>
)
}
]}
/>
</TabPane>
</Tabs>
)}
{modalType === 'model' && (
<>
<Form.Item label="模型名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="模型编码" name="code" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="模型描述" name="desc"><Input.TextArea rows={2} /></Form.Item>
<Divider>属性列表</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('props')} className="mb-2">新增属性</Button>
<Table size="small" dataSource={modelDetail.props} pagination={false} columns={[
{ title: '编码', dataIndex: 'code' }, { title: '名称', dataIndex: 'name' }, { title: '类型', dataIndex: 'type' },
{ title: '告警', render: r => <Switch checked={r.isAlarm} disabled size="small" /> },
{ title: '操作', render: r => <Space><Button type="text" icon={<EditOutlined />} onClick={() => openItemModal('props', r)}>编辑</Button><Popconfirm onConfirm={() => deleteItem('props', r.id)}><Button type="text" danger icon={<DeleteOutlined />}>删除</Button></Popconfirm></Space> }
]} />
<Divider>事件列表</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('events')} className="mb-2">新增事件</Button>
<Table size="small" dataSource={modelDetail.events} pagination={false} columns={[{ title: '编码', dataIndex: 'code' }, { title: '名称', dataIndex: 'name' }, { title: '操作', render: r => <Button type="text" icon={<EditOutlined />} onClick={() => openItemModal('events', r)}>编辑</Button> }]} />
</>
)}
</Form>
</Modal>
<Modal open={itemModal} width={520} zIndex={1001} destroyOnClose onCancel={() => setItemModal(false)} onOk={saveItem} title="编辑">
<Form form={itemForm} layout="vertical">
<Form.Item name="id" hidden><Input /></Form.Item>
<Form.Item label="编码" name="code" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
{itemType === 'props' && <><Form.Item label="数据类型" name="type"><Select><Option value="int">int</Option><Option value="float">float</Option><Option value="string">string</Option></Select></Form.Item><Form.Item label="作为告警参数" name="isAlarm" valuePropName="checked"><Switch /></Form.Item></>}
</Form>
</Modal>
{/* 字段映射弹窗:操作即保存 */}
<Modal open={mappingModal} width={600} zIndex={1002} destroyOnClose onCancel={() => setMappingModal(false)} onOk={saveMapping} title="字段映射配置">
<Form form={mappingForm} layout="vertical">
<Form.Item name="id" hidden /><Form.Item name="productId" hidden />
<Form.Item label="标准模型字段" name="standardCode" rules={[{ required: true }]}>
<Select>{modelDetail.props?.map(p => <Option key={p.code} value={p.code}>{p.name}</Option>)}</Select>
</Form.Item>
<Form.Item label="厂商原始字段" name="sourceField" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="转换规则" name="convertRule"><Input /></Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@@ -8,8 +8,8 @@ import * as AllIcons from '@ant-design/icons';
import {
PlusOutlined, EditOutlined, DeleteOutlined, SettingOutlined
} from '@ant-design/icons';
import { addMenu, deleteMenu, getMenuList, updateMenu } from '../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../lib/utils';
import { addMenu, deleteMenu, getMenuList, updateMenu } from '../../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../../lib/utils';
const { Option } = Select;

View File

@@ -6,7 +6,7 @@ import {
import {
PlusOutlined, EditOutlined, DeleteOutlined
} from '@ant-design/icons';
import { addOrg, deleteOrg, getOrgList } from '../api/organization';
import { addOrg, deleteOrg, getOrgList } from '../../api/organization';
const { Title } = Typography;
const { Option } = Select;

View File

@@ -9,10 +9,10 @@ import {
import {
getStationList, saveSite, deleteSite,
getStationRegions, saveRegion, deleteRegion
} from '../api/stationManage';
import { getOrgList } from '../api/organization';
} from '../../api/stationManage';
import { getOrgList } from '../../api/organization';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
import { RootState } from '../../store';
const { Option } = Select;

View File

@@ -7,15 +7,15 @@ import {
import {
PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined
} from '@ant-design/icons';
import { getMenuList } from '../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../lib/utils';
import { addRole, deleteRole, editRoleApi, getRoleAllocate, getRoleDetailById, getRoleList } from '../api/role';
import { addUser, deleteUser, editUser, getUser } from '../api/user';
import EmptyData from '../components/Empty';
import { getMenuList } from '../../api/mune';
import { buildPermTree, buildTreeFromFlat } from '../../lib/utils';
import { addRole, deleteRole, editRoleApi, getRoleAllocate, getRoleDetailById, getRoleList } from '../../api/role';
import { addUser, deleteUser, editUser, getUser } from '../../api/user';
import EmptyData from '../Empty';
import { useSelector } from 'react-redux';
import { RootState } from '../store';
import { getOrgList } from '../api/organization';
import { getSiteByOrgId } from '../api/stationManage';
import { RootState } from '../../store';
import { getOrgList } from '../../api/organization';
import { getSiteByOrgId } from '../../api/stationManage';
const { Text } = Typography;
const { TabPane } = Tabs;

View File

@@ -0,0 +1,183 @@
import React, { useState } from 'react';
import {
Card, Table, Tag, Badge, Space, Button,
Modal, Descriptions, Form, Select, Input
} from 'antd';
import { EyeOutlined } from '@ant-design/icons';
const AntdCard = Card as any;
const { Option } = Select;
const { Search } = Input;
// 模拟历史数据
const mockHistory = [
{ id: 1, deviceName: '1#环境传感器', level: 'warn', content: '温度超过阈值 62℃', time: '2026-05-07 08:10:22', status: 'processed' },
{ id: 2, deviceName: '2#阵列区球机', level: 'error', content: '视频信号丢失', time: '2026-05-06 15:30:11', status: 'closed' },
{ id: 3, deviceName: '3#电池供电设备', level: 'info', content: '电量低于20%', time: '2026-05-05 10:20:15', status: 'processed' },
{ id: 4, deviceName: '1#阵列区球机', level: 'error', content: '存储设备故障', time: '2026-05-04 23:10:00', status: 'closed' },
];
const AlarmHistory = () => {
// 详情弹窗
const [detailVisible, setDetailVisible] = useState(false);
const [currentAlarm, setCurrentAlarm] = useState<any>(null);
// 搜索筛选
const [list, setList] = useState(mockHistory);
const [filteredList, setFilteredList] = useState(mockHistory);
const [timeRange, setTimeRange] = useState('today');
const [deviceType, setDeviceType] = useState('all');
// 打开详情
const showDetail = (record: any) => {
setCurrentAlarm(record);
setDetailVisible(true);
};
// 统一筛选逻辑
const handleFilter = (searchValue = '') => {
let res = list.filter(item => {
// 时间筛选
const timeMatch = timeRange === 'all' || true; // 后端可替换
// 设备筛选
const deviceMatch = deviceType === 'all' || item.deviceName.includes(deviceType);
// 搜索筛选
const searchMatch = !searchValue ||
item.deviceName.includes(searchValue) ||
item.content.includes(searchValue) ||
item.level.includes(searchValue);
return timeMatch && deviceMatch && searchMatch;
});
setFilteredList(res);
};
// 搜索
const handleSearch = (value: string) => {
handleFilter(value);
};
// 表格列
const columns = [
{ title: '告警时间', dataIndex: 'time', width: 180 },
{ title: '设备名称', dataIndex: 'deviceName' },
{
title: '告警级别',
render: (row: any) => {
const color = row.level === 'warn' ? 'orange' : row.level === 'error' ? 'red' : 'blue';
const text = row.level === 'warn' ? '警告' : row.level === 'error' ? '严重' : '提示';
return <Tag color={color}>{text}</Tag>;
}
},
{ title: '告警内容', dataIndex: 'content' },
{
title: '处理状态',
render: (row: any) => {
const statusMap = {
processed: { status: 'success', text: '已处理' },
closed: { status: 'default', text: '已关闭' },
};
const s = statusMap[row.status] || { status: 'default', text: '未知' };
return <Badge status={s.status as any} text={s.text} />;
}
},
{
title: '操作',
render: (_: any, record: any) => (
<Button type="link" icon={<EyeOutlined />} onClick={() => showDetail(record)}>
查看详情
</Button>
)
}
];
return (
<div style={{ padding: 24 }}>
<AntdCard title="告警历史">
{/* 统一筛选栏:搜索 + 时间 + 设备 */}
<Space wrap size={16} style={{ marginBottom: 16, width: '100%' }}>
<Search
placeholder="搜索设备/内容/级别"
style={{ width: 260 }}
onSearch={handleSearch}
/>
<Form layout="inline">
<Form.Item label="时间范围">
<Select
value={timeRange}
style={{ width: 160 }}
onChange={(val) => {
setTimeRange(val);
handleFilter();
}}
>
<Option value="today">今日</Option>
<Option value="yesterday">昨日</Option>
<Option value="week">近7天</Option>
<Option value="month">本月</Option>
<Option value="all">全部</Option>
</Select>
</Form.Item>
<Form.Item label="设备类型">
<Select
value={deviceType}
style={{ width: 160 }}
onChange={(val) => {
setDeviceType(val);
handleFilter();
}}
>
<Option value="all">全部设备</Option>
<Option value="传感器">传感器</Option>
<Option value="球机">球机</Option>
<Option value="电池">电池设备</Option>
</Select>
</Form.Item>
</Form>
</Space>
{/* 表格 */}
<Table
rowKey="id"
columns={columns}
dataSource={filteredList}
pagination={{ pageSize: 10 }}
/>
</AntdCard>
{/* 详情弹窗 */}
<Modal
title="告警历史详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={null}
width={520}
>
{currentAlarm && (
<Descriptions column={1} bordered>
<Descriptions.Item label="告警时间">{currentAlarm.time}</Descriptions.Item>
<Descriptions.Item label="设备名称">{currentAlarm.deviceName}</Descriptions.Item>
<Descriptions.Item label="告警内容">{currentAlarm.content}</Descriptions.Item>
<Descriptions.Item label="告警级别">
<Tag color={currentAlarm.level === 'warn' ? 'orange' : currentAlarm.level === 'error' ? 'red' : 'blue'}>
{currentAlarm.level === 'warn' ? '警告' : currentAlarm.level === 'error' ? '严重' : '提示'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Badge
status={currentAlarm.status === 'processed' ? 'success' : 'default'}
text={currentAlarm.status === 'processed' ? '已处理' : '已关闭'}
/>
</Descriptions.Item>
</Descriptions>
)}
</Modal>
</div>
);
};
export default AlarmHistory;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Card, Row, Col, Space, Empty } from 'antd';
import { AlertOutlined, CloseCircleOutlined, ExclamationCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
const AntdCard = Card as any;
const AlarmOverview = () => {
return (
<div style={{ padding: 24 }}>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}><AntdCard size="small"><Space><div style={{ background: '#e6f4ff', borderRadius: 8, padding: 8 }}><AlertOutlined style={{ color: '#1677ff', fontSize: 20 }} /></div><div><div style={{ fontSize: 13, color: '#666' }}>告警总数</div><div style={{ fontWeight: 600 }}>45</div></div></Space></AntdCard></Col>
<Col span={6}><AntdCard size="small"><Space><div style={{ background: '#fff2f0', borderRadius: 8, padding: 8 }}><CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: 20 }} /></div><div><div style={{ fontSize: 13, color: '#666' }}>严重告警</div><div style={{ fontWeight: 600 }}>12</div></div></Space></AntdCard></Col>
<Col span={6}><AntdCard size="small"><Space><div style={{ background: '#fff7e6', borderRadius: 8, padding: 8 }}><ExclamationCircleOutlined style={{ color: '#fa8c16', fontSize: 20 }} /></div><div><div style={{ fontSize: 13, color: '#666' }}>警告告警</div><div style={{ fontWeight: 600 }}>20</div></div></Space></AntdCard></Col>
<Col span={6}><AntdCard size="small"><Space><div style={{ background: '#f6ffed', borderRadius: 8, padding: 8 }}><CheckCircleOutlined style={{ color: '#52c41a', fontSize: 20 }} /></div><div><div style={{ fontSize: 13, color: '#666' }}>已处理</div><div style={{ fontWeight: 600 }}>13</div></div></Space></AntdCard></Col>
</Row>
<AntdCard title="告警地图"><Empty description="告警地图(可接入GIS)" /></AntdCard>
</div>
);
};
export default AlarmOverview;

View File

@@ -0,0 +1,138 @@
import React, { useState } from 'react';
import {
Card, Table, Tag, Badge, Space, Button,
Modal, Descriptions, Input, Select
} from 'antd';
import { EyeOutlined } from '@ant-design/icons';
const AntdCard = Card as any;
const { Search } = Input;
const { Option } = Select;
// 模拟数据
const mockAlarms = [
{ id: 1, deviceName: '1#环境传感器', level: 'warn', content: '温度超过阈值 62℃', time: '2026-05-07 10:24:00', status: 'unprocessed' },
{ id: 2, deviceName: '2#阵列区球机', level: 'error', content: '视频信号丢失', time: '2026-05-07 10:20:00', status: 'processing' },
{ id: 3, deviceName: '3#电池供电设备', level: 'info', content: '电量低于20%', time: '2026-05-07 09:50:00', status: 'processed' },
];
const AlarmRealtime = () => {
// 弹窗控制
const [detailVisible, setDetailVisible] = useState(false);
const [currentAlarm, setCurrentAlarm] = useState<any>(null);
// 搜索 + 筛选
const [list, setList] = useState(mockAlarms);
const [filteredList, setFilteredList] = useState(mockAlarms);
// 打开详情
const showDetail = (record: any) => {
setCurrentAlarm(record);
setDetailVisible(true);
};
// 搜索筛选
const handleSearch = (value: string) => {
const res = list.filter(item =>
item.deviceName.includes(value) ||
item.content.includes(value) ||
item.level.includes(value)
);
setFilteredList(res);
};
// 表格列
const columns = [
{ title: '时间', dataIndex: 'time', width: 180 },
{ title: '设备', dataIndex: 'deviceName' },
{
title: '级别',
render: (l: any) => {
const color = l.level === 'warn' ? 'orange' : l.level === 'error' ? 'red' : 'blue';
const text = l.level === 'warn' ? '警告' : l.level === 'error' ? '严重' : '提示';
return <Tag color={color}>{text}</Tag>;
}
},
{ title: '内容', dataIndex: 'content' },
{
title: '状态',
render: (r: any) => {
const statusMap = {
unprocessed: { status: 'error', text: '未处理' },
processing: { status: 'warning', text: '处理中' },
processed: { status: 'success', text: '已处理' },
closed: { status: 'default', text: '已关闭' },
};
const s = statusMap[r.status] || statusMap.unprocessed;
return <Badge status={s.status as any} text={s.text} />;
}
},
{
title: '操作',
render: (_: any, record: any) => (
<Button type="link" icon={<EyeOutlined />} onClick={() => showDetail(record)}>
详情
</Button>
)
},
];
return (
<div style={{ padding: 24 }}>
<AntdCard
title="实时告警"
extra={
<Search
placeholder="搜索设备名称/告警内容"
style={{ width: 260 }}
onSearch={handleSearch}
/>
}
>
{/* 表格 */}
<Table
rowKey="id"
columns={columns}
dataSource={filteredList}
pagination={{ pageSize: 10 }}
/>
</AntdCard>
{/* 告警详情弹窗 */}
<Modal
title="告警详情"
open={detailVisible}
onCancel={() => setDetailVisible(false)}
footer={null}
width={500}
>
{currentAlarm && (
<Descriptions column={1} bordered>
<Descriptions.Item label="告警时间">{currentAlarm.time}</Descriptions.Item>
<Descriptions.Item label="设备名称">{currentAlarm.deviceName}</Descriptions.Item>
<Descriptions.Item label="告警内容">{currentAlarm.content}</Descriptions.Item>
<Descriptions.Item label="告警级别">
<Tag color={currentAlarm.level === 'warn' ? 'orange' : currentAlarm.level === 'error' ? 'red' : 'blue'}>
{currentAlarm.level === 'warn' ? '警告' : currentAlarm.level === 'error' ? '严重' : '提示'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="处理状态">
<Badge
status={
currentAlarm.status === 'unprocessed' ? 'error' :
currentAlarm.status === 'processing' ? 'warning' : 'success'
}
text={
currentAlarm.status === 'unprocessed' ? '未处理' :
currentAlarm.status === 'processing' ? '处理中' : '已处理'
}
/>
</Descriptions.Item>
</Descriptions>
)}
</Modal>
</div>
);
};
export default AlarmRealtime;

View File

@@ -0,0 +1,181 @@
import React, { useState } from 'react';
import {
Card, Table, Button, Space, Popconfirm, Tag, Badge,
Modal, Form, Input, Select, Switch
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const AntdCard = Card as any;
const { Option } = Input;
const { Search } = Input;
// 模拟数据
const mockRules = [
{ id: 1, name: '温度超温', deviceType: 'sensor', level: 'warn', condition: 'temp > 60', status: true },
{ id: 2, name: '视频信号丢失', deviceType: 'camera', level: 'error', condition: 'signal == 0', status: true },
{ id: 3, name: '电池低电量', deviceType: 'battery', level: 'info', condition: 'power < 20', status: false },
];
const AlarmRules = () => {
const [list, setList] = useState(mockRules);
const [filteredList, setFilteredList] = useState(mockRules);
const [modalVisible, setModalVisible] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [form] = Form.useForm();
// 搜索筛选
const handleSearch = (value: string) => {
const res = list.filter(item =>
item.name.includes(value) ||
item.deviceType.includes(value) ||
item.condition.includes(value)
);
setFilteredList(res);
};
// 打开新增
const openAdd = () => {
setEditingId(null);
form.resetFields();
setModalVisible(true);
};
// 打开编辑
const openEdit = (record: any) => {
setEditingId(record.id);
form.setFieldsValue(record);
setModalVisible(true);
};
// 保存(新增 + 编辑)
const handleSave = () => {
form.validateFields().then(values => {
if (editingId !== null) {
// 编辑
const updated = list.map(item =>
item.id === editingId ? { ...item, ...values } : item
);
setList(updated);
setFilteredList(updated);
} else {
// 新增
const newRule = { ...values, id: Date.now() };
const newList = [...list, newRule];
setList(newList);
setFilteredList(newList);
}
setModalVisible(false);
});
};
// 删除
const handleDelete = (id: number) => {
const newList = list.filter(x => x.id !== id);
setList(newList);
setFilteredList(newList);
};
const columns = [
{ title: '规则名称', dataIndex: 'name' },
{ title: '设备类型', dataIndex: 'deviceType' },
{
title: '告警级别',
render: (r: any) => {
const color = r.level === 'error' ? 'red' : r.level === 'warn' ? 'orange' : 'blue';
const text = r.level === 'error' ? '严重' : r.level === 'warn' ? '警告' : '提示';
return <Tag color={color}>{text}</Tag>;
}
},
{ title: '触发条件', dataIndex: 'condition' },
{
title: '状态',
render: (r: any) => (
<Badge status={r.status ? 'success' : 'error'} text={r.status ? '启用' : '禁用'} />
)
},
{
title: '操作',
render: (r: any) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
<Popconfirm
title="确定删除这条规则吗?"
onConfirm={() => handleDelete(r.id)}
>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<div style={{ padding: 24 }}>
<AntdCard
title="告警规则"
extra={
<Space>
{/* 修复:使用 Input.Search */}
<Search
placeholder="搜索规则名称/设备/条件"
style={{ width: 260 }}
onSearch={handleSearch}
/>
<Button icon={<PlusOutlined />} type="primary" onClick={openAdd}>
新增规则
</Button>
</Space>
}
>
<Table
rowKey="id"
columns={columns}
dataSource={filteredList}
pagination={{ pageSize: 10 }}
/>
</AntdCard>
{/* 新增/编辑 弹窗 */}
<Modal
title={editingId ? '编辑告警规则' : '新增告警规则'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSave}
maskClosable={false}
>
<Form form={form} layout="vertical">
<Form.Item label="规则名称" name="name" rules={[{ required: true, message: '请输入规则名称' }]}>
<Input placeholder="例如:温度超温告警" />
</Form.Item>
<Form.Item label="设备类型" name="deviceType" rules={[{ required: true }]}>
<Select>
<Select.Option value="sensor">传感器</Select.Option>
<Select.Option value="camera">摄像头</Select.Option>
<Select.Option value="battery">电池设备</Select.Option>
<Select.Option value="inverter">逆变器</Select.Option>
</Select>
</Form.Item>
<Form.Item label="告警级别" name="level" rules={[{ required: true }]}>
<Select>
<Select.Option value="error">严重</Select.Option>
<Select.Option value="warn">警告</Select.Option>
<Select.Option value="info">提示</Select.Option>
</Select>
</Form.Item>
<Form.Item label="触发条件" name="condition" rules={[{ required: true }]}>
<Input placeholder="例如:temp > 60 / power < 20" />
</Form.Item>
<Form.Item label="是否启用" name="status" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AlarmRules;

View File

@@ -0,0 +1,129 @@
import React, { useEffect, useRef } from 'react';
import { Card, Row, Col, Space } from 'antd';
import { AreaChartOutlined } from '@ant-design/icons';
import * as echarts from 'echarts';
const AntdCard = Card as any;
const AlarmStatistics = () => {
// 图表 ref
const barChartRef = useRef(null);
const pieChartRef = useRef(null);
// 柱状图
useEffect(() => {
if (!barChartRef.current) return;
const barChart = echarts.init(barChartRef.current);
barChart.setOption({
title: { text: '近7天告警趋势' },
xAxis: {
type: 'category',
data: ['1日', '2日', '3日', '4日', '5日', '6日', '7日']
},
yAxis: { type: 'value' },
series: [
{
data: [5, 8, 12, 9, 15, 7, 12],
type: 'bar',
itemStyle: { color: '#1677ff' }
}
]
});
const resizeFn = () => barChart.resize();
window.addEventListener('resize', resizeFn);
return () => {
barChart.dispose();
window.removeEventListener('resize', resizeFn);
};
}, []);
// 饼图
useEffect(() => {
if (!pieChartRef.current) return;
const pieChart = echarts.init(pieChartRef.current);
pieChart.setOption({
title: { text: '告警级别分布' },
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: '50%',
data: [
{ value: 15, name: '严重告警' },
{ value: 30, name: '警告告警' },
{ value: 25, name: '提示告警' }
]
}
]
});
const resizeFn = () => pieChart.resize();
window.addEventListener('resize', resizeFn);
return () => {
pieChart.dispose();
window.removeEventListener('resize', resizeFn);
};
}, []);
return (
<div style={{ padding: 24 }}>
{/* 顶部统计卡片 保留你原来的图标样式 */}
<AntdCard title="告警概览" style={{ marginBottom: 24 }}>
<Row gutter={16}>
<Col span={8}>
<Space>
<div style={{ background: '#e6f4ff', padding: 8, borderRadius: 8 }}>
<AreaChartOutlined style={{ color: '#1677ff', fontSize: 20 }} />
</div>
<div>
<div style={{ color: '#666' }}>今日告警</div>
<div style={{ fontWeight: 600, fontSize: 18 }}>12</div>
</div>
</Space>
</Col>
<Col span={8}>
<Space>
<div style={{ background: '#fff7e6', padding: 8, borderRadius: 8 }}>
<AreaChartOutlined style={{ color: '#fa8c16', fontSize: 20 }} />
</div>
<div>
<div style={{ color: '#666' }}>本周告警</div>
<div style={{ fontWeight: 600, fontSize: 18 }}>45</div>
</div>
</Space>
</Col>
<Col span={8}>
<Space>
<div style={{ background: '#f6ffed', padding: 8, borderRadius: 8 }}>
<AreaChartOutlined style={{ color: '#52c41a', fontSize: 20 }} />
</div>
<div>
<div style={{ color: '#666' }}>本月告警</div>
<div style={{ fontWeight: 600, fontSize: 18 }}>128</div>
</div>
</Space>
</Col>
</Row>
</AntdCard>
{/* 两个图表 左右各一半 并排显示 不空洞 */}
<Row gutter={16}>
<Col span={12}>
<AntdCard title="告警趋势统计">
<div ref={barChartRef} style={{ width: '100%', height: 350 }} />
</AntdCard>
</Col>
<Col span={12}>
<AntdCard title="告警分类统计">
<div ref={pieChartRef} style={{ width: '100%', height: 350 }} />
</AntdCard>
</Col>
</Row>
</div>
);
};
export default AlarmStatistics;

View File

@@ -0,0 +1,168 @@
import React, { useState } from 'react';
import {
Card, Table, Button, Space, Popconfirm, Tag,
Modal, Form, Input, Select
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
const AntdCard = Card as any;
const { Option } = Select;
const { Search } = Input;
// 模拟数据
const mockSub = [
{ id: 1, name: '运维订阅', deviceType: 'all', level: 'all', channels: ['短信'] },
{ id: 2, name: '管理员订阅', deviceType: 'sensor', level: 'error', channels: ['短信', '邮件'] },
{ id: 3, name: '设备订阅', deviceType: 'camera', level: 'warn', channels: ['APP推送'] },
];
const AlarmSubscription = () => {
const [list, setList] = useState(mockSub);
const [filteredList, setFilteredList] = useState(mockSub);
const [modalVisible, setModalVisible] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [form] = Form.useForm();
// 搜索筛选
const handleSearch = (value) => {
const res = list.filter(item =>
item.name.includes(value) ||
item.deviceType.includes(value) ||
item.level.includes(value)
);
setFilteredList(res);
};
// 打开新增
const openAdd = () => {
setEditingId(null);
form.resetFields();
setModalVisible(true);
};
// 打开编辑(回填数据)
const openEdit = (record) => {
setEditingId(record.id);
form.setFieldsValue(record);
setModalVisible(true);
};
// 保存(新增+编辑)
const handleSave = () => {
form.validateFields().then(values => {
if (editingId !== null) {
// 编辑
const updated = list.map(item =>
item.id === editingId ? { ...item, ...values } : item
);
setList(updated);
setFilteredList(updated);
} else {
// 新增
const newItem = { ...values, id: Date.now() };
const newList = [...list, newItem];
setList(newList);
setFilteredList(newList);
}
setModalVisible(false);
});
};
// 删除
const handleDelete = (id) => {
const newList = list.filter(x => x.id !== id);
setList(newList);
setFilteredList(newList);
};
const columns = [
{ title: '订阅名称', dataIndex: 'name' },
{ title: '设备类型', dataIndex: 'deviceType' },
{ title: '告警级别', dataIndex: 'level' },
{
title: '通知渠道',
render: (r) => r.channels.map(i => <Tag key={i}>{i}</Tag>)
},
{
title: '操作',
render: (r) => (
<Space>
<Button type="link" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}>
<Button type="link" danger icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<div style={{ padding: 24 }}>
<AntdCard
title="告警订阅"
extra={
<Space>
<Search
placeholder="搜索订阅/设备/级别"
style={{ width: 260 }}
onSearch={handleSearch}
/>
<Button icon={<PlusOutlined />} type="primary" onClick={openAdd}>
新增订阅
</Button>
</Space>
}
>
<Table
rowKey="id"
columns={columns}
dataSource={filteredList}
pagination={{ pageSize: 10 }}
/>
</AntdCard>
{/* 新增/编辑 弹窗 */}
<Modal
title={editingId ? '编辑告警订阅' : '新增告警订阅'}
open={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleSave}
maskClosable={false}
>
<Form form={form} layout="vertical">
<Form.Item label="订阅名称" name="name" rules={[{ required: true }]}>
<Input placeholder="请输入订阅名称" />
</Form.Item>
<Form.Item label="设备类型" name="deviceType" rules={[{ required: true }]}>
<Select>
<Option value="all">全部设备</Option>
<Option value="sensor">传感器</Option>
<Option value="camera">摄像头</Option>
<Option value="battery">电池设备</Option>
</Select>
</Form.Item>
<Form.Item label="告警级别" name="level" rules={[{ required: true }]}>
<Select>
<Option value="all">全部级别</Option>
<Option value="error">严重</Option>
<Option value="warn">警告</Option>
<Option value="info">提示</Option>
</Select>
</Form.Item>
<Form.Item label="通知渠道" name="channels" rules={[{ required: true }]}>
<Select mode="multiple" placeholder="请选择通知渠道">
<Option value="短信">短信</Option>
<Option value="邮件">邮件</Option>
<Option value="APP推送">APP推送</Option>
</Select>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AlarmSubscription;

View File

@@ -1,323 +1,90 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Layout, Menu, Typography } from 'antd';
import {
Table,
Tag,
Input,
Button,
Row,
Col,
Card,
Typography,
Space,
Statistic,
Select,
DatePicker,
List,
Layout,
} from 'antd';
import {
BellOutlined,
AlertOutlined,
SyncOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
RiseOutlined,
FallOutlined,
CloseOutlined,
AlertOutlined, ClockCircleOutlined, FileTextOutlined, BarChartOutlined,
SettingOutlined, BellOutlined
} from '@ant-design/icons';
import { getMenuList } from '../api/mune';
import AlarmOverview from '../components/alerts/AlarmOverview';
import AlarmRealtime from '../components/alerts/AlarmRealtime';
import AlarmHistory from '../components/alerts/AlarmHistory';
import AlarmStatistics from '../components/alerts/AlarmStatistics';
import AlarmRules from '../components/alerts/AlarmRules';
import AlarmSubscription from '../components/alerts/AlarmSubscription';
const { Header, Content } = Layout;
const { Text } = Typography;
const { RangePicker } = DatePicker;
const AntdCard = Card as any;
// 模拟数据
const alertList = [
{
id: 'AL20250525001',
content: '逆变器停机',
device: '逆变器 12 (组串式)',
level: '严重',
time: '2025-05-25 09:18:32',
duration: '2小时18分',
status: '处理中',
handler: '张运维',
},
{
id: 'AL20250525002',
content: '发电量下降 > 15%',
device: '汇流箱 05',
level: '重要',
time: '2025-05-25 08:46:11',
duration: '3小时50分',
status: '处理中',
handler: '李工程师',
},
{
id: 'AL20250525003',
content: '逆变器温度过高',
device: '逆变器 07 (组串式)',
level: '重要',
time: '2025-05-25 07:32:45',
duration: '4小时24分',
status: '待处理',
handler: '-',
},
];
export default function AlarmCenterPage() {
const [activeTab, setActiveTab] = useState('overview');
const [rawMenuList, setRawMenuList] = useState([]);
const [showTabs, setShowTabs] = useState([]);
const stats = [
{
title: '今日告警总数',
value: 36,
change: '+28.6%',
up: true,
extra: '昨日 28',
icon: <BellOutlined style={{ color: '#ff4d4f' }} />,
},
{
title: '严重告警',
value: 8,
change: '+33.3%',
up: true,
extra: '昨日 6',
icon: <AlertOutlined style={{ color: '#ff4d4f' }} />,
},
{
title: '处理中',
value: 12,
change: '+20.0%',
up: true,
extra: '昨日 10',
icon: <SyncOutlined style={{ color: '#fa8c16' }} />,
},
{
title: '已关闭',
value: 16,
change: '+45.5%',
up: true,
extra: '昨日 11',
icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
},
{
title: '平均响应时长',
value: '18分32秒',
change: '-12.6%',
up: false,
extra: '昨日 21分12秒',
icon: <ClockCircleOutlined style={{ color: '#1890ff' }} />,
},
];
export default function Alerts() {
// 控制右侧详情面板
const [showDetail, setShowDetail] = useState(false);
const [selectedAlert, setSelectedAlert] = useState<any>(null);
const columns = [
{ title: '告警编号', dataIndex: 'id', key: 'id', width: 120 },
{ title: '告警内容', dataIndex: 'content', key: 'content' },
{ title: '来源设备', dataIndex: 'device', key: 'device', width: 160 },
{
title: '告警级别',
dataIndex: 'level',
key: 'level',
width: 80,
render: (level: string) => {
const colorMap: Record<string, string> = {
严重: 'red',
重要: 'orange',
一般: 'gold',
提示: 'blue',
// 获取菜单(和系统设置一模一样)
const getMenuListApi = () => {
const params = { pageNum: 1, pageSize: 99999 };
return getMenuList(params);
};
return <Tag color={colorMap[level]}>{level}</Tag>;
},
},
{ title: '发生时间', dataIndex: 'time', key: 'time', width: 160 },
{ title: '持续时长', dataIndex: 'duration', key: 'duration', width: 110 },
{
title: '处理状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const colorMap: Record<string, string> = {
处理中: 'blue',
待处理: 'orange',
已关闭: 'green',
// 动态生成 tab
useEffect(() => {
getMenuListApi().then(res => {
if (res.code === 200) {
const data = res.rows || [];
// 告警中心 parentId 你自己改成你数据库里的 ID
const alarmChild = data.filter(m => m.parentId == 3388);
alarmChild.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0));
const tabs = alarmChild.map(item => {
if (item.path === '/alerts/all') return { key: 'overview', label: '告警总览', icon: <AlertOutlined /> };
if (item.path === '/alerts/time') return { key: 'realtime', label: '实时告警', icon: <ClockCircleOutlined /> };
if (item.path === '/alerts/history') return { key: 'history', label: '告警历史', icon: <FileTextOutlined /> };
if (item.path === '/alerts/counts') return { key: 'statistics', label: '告警统计', icon: <BarChartOutlined /> };
if (item.path === '/alerts/rules') return { key: 'rules', label: '告警规则', icon: <SettingOutlined /> };
if (item.path === '/alerts/subscribe') return { key: 'subscription', label: '告警订阅', icon: <BellOutlined /> };
return null;
}).filter(Boolean);
setShowTabs(tabs);
if (tabs.length > 0) setActiveTab(tabs[0].key);
}
});
}, []);
// 渲染对应组件
const renderTabContent = () => {
switch (activeTab) {
case 'overview': return <AlarmOverview />;
case 'realtime': return <AlarmRealtime />;
case 'history': return <AlarmHistory />;
case 'statistics': return <AlarmStatistics />;
case 'rules': return <AlarmRules />;
case 'subscription': return <AlarmSubscription />;
default: return <div style={{ padding: 20 }}><Text>请选择菜单</Text></div>;
}
};
return <Tag color={colorMap[status]}>{status}</Tag>;
},
},
{ title: '负责人', dataIndex: 'handler', key: 'handler', width: 90 },
{
title: '操作',
key: 'action',
width: 80,
render: (_, record) => (
<Button
type="link"
size="small"
onClick={() => {
setSelectedAlert(record);
setShowDetail(true);
}}
>
查看
</Button>
),
},
];
return (
<div style={{ background: '#f4f7f9', minHeight: '100%' }}>
<Layout style={{ background: 'transparent' }}>
{/* 左侧主内容区:宽度动态变化 */}
<Layout.Content
style={{
padding: '16px',
transition: 'width 0.3s ease',
width: showDetail ? 'calc(100% - 460px)' : '100%',
}}
>
<div style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>告警中心</div>
{/* 筛选栏 */}
<AntdCard size="small" style={{ borderRadius: 8, marginBottom: 16 }}>
<Row gutter={[16, 16]} align="middle">
<Col span={4}><Select placeholder="告警级别" style={{ width: '100%' }} /></Col>
<Col span={4}><Select placeholder="告警类型" style={{ width: '100%' }} /></Col>
<Col span={4}><Select placeholder="设备类型" style={{ width: '100%' }} /></Col>
<Col span={6}><RangePicker style={{ width: '100%' }} /></Col>
<Col span={6}><Select placeholder="处理状态" style={{ width: '100%' }} /></Col>
<Col span={24} style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<Input placeholder="搜索" style={{ width: 220 }} />
<Button>重置</Button>
<Button type="primary">查询</Button>
</Col>
</Row>
</AntdCard>
{/* 统计卡片 */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
{stats.map((stat, idx) => (
<Col xs={24} sm={12} md={8} lg={4} key={idx}>
<AntdCard size="small" style={{ borderRadius: 8 }}>
<Statistic
title={<span style={{ fontSize: 13 }}>{stat.title}</span>}
value={stat.value}
valueStyle={{ fontSize: 20, fontWeight: 600 }}
prefix={stat.icon}
<Layout style={{ minHeight: '100vh', background: '#fff', overflow: 'hidden' }}>
<Header style={{ background: '#fff', padding: 0, borderBottom: '1px solid #f0f0f0', position: 'sticky', top: 0, zIndex: 10 }}>
<Menu
mode="horizontal"
selectedKeys={[activeTab]}
onClick={({ key }) => setActiveTab(key)}
items={showTabs}
style={{ border: 0, height: 64, lineHeight: '64px' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
<Text type="secondary" style={{ fontSize: 12 }}>{stat.extra}</Text>
<Text style={{ color: stat.up ? '#ff4d4f' : '#52c41a', fontSize: 12 }}>
{stat.change} {stat.up ? <RiseOutlined /> : <FallOutlined />}
</Text>
</div>
</AntdCard>
</Col>
))}
</Row>
</Header>
{/* 告警列表 */}
<AntdCard
size="small"
title={
<div style={{ fontSize: 14, fontWeight: 600 }}>
<span style={{ color: '#165DFF' }}>告警列表</span>
<span style={{ color: '#333' }}>(共36条)</span>
</div>
}
style={{ borderRadius: 8 }}
>
<Table
size="small"
columns={columns}
dataSource={alertList}
pagination={{ pageSize: 10 }}
rowKey="id"
onRow={(record) => ({
onClick: () => {
setSelectedAlert(record);
setShowDetail(true);
},
})}
/>
</AntdCard>
</Layout.Content>
{/* 右侧详情面板:推挤式,非覆盖 */}
{showDetail && (
<Layout.Sider
width={460}
style={{
background: '#fff',
padding: '16px',
height: 'calc(100vh - 50px)',
overflow: 'auto',
boxShadow: '-2px 0 8px rgba(0,0,0,0.08)',
}}
>
{selectedAlert && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Text strong style={{ fontSize: 16 }}>告警详情</Text>
<Button
type="text"
icon={<CloseOutlined />}
onClick={() => setShowDetail(false)}
style={{ fontSize: 16 }}
/> </div>
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<BellOutlined style={{ color: '#ff4d4f', fontSize: 20 }} />
<Text strong style={{ fontSize: 16 }}>{selectedAlert.content}</Text>
<Tag color="red">{selectedAlert.level}</Tag>
</div>
<div style={{ fontSize: 12, color: '#666', lineHeight: 1.8 }}>
<div>告警编号:{selectedAlert.id}</div>
<div>来源设备:{selectedAlert.device}</div>
<div>发生时间:{selectedAlert.time}</div>
<div>持续时长:{selectedAlert.duration}</div>
<div>处理状态:{selectedAlert.status}</div>
<div>负责人:{selectedAlert.handler}</div>
</div>
</div>
<div style={{ marginBottom: 16 }}>
<Text strong style={{ fontSize: 13, marginBottom: 8, display: 'block' }}>根因分析建议</Text>
<List
size="small"
dataSource={[
'逆变器内部保护触发(IGBT/继电器故障)',
'交流侧电压异常或并网不稳定',
'散热风扇故障导致过温保护',
]}
renderItem={(item) => <List.Item style={{ padding: '4px 0' }}>{item}</List.Item>}
/>
</div>
<div style={{ marginBottom: 16 }}>
<Text strong style={{ fontSize: 13, marginBottom: 8, display: 'block' }}>推荐处理措施</Text>
<List
size="small"
dataSource={[
'现场检查逆变器告警记录及指示灯状态',
'检查散热风扇运行及通风条件',
'检查交流侧电压及并网条件',
]}
renderItem={(item) => <List.Item style={{ padding: '4px 0' }}>{item}</List.Item>}
/>
</div>
<Button type="primary" block style={{ marginTop: 16 }}>
标记为已解决
</Button>
</div>
)}
</Layout.Sider>
)}
<Content style={{
maxHeight: 'calc(100vh - 64px)', overflowY: 'auto', background: '#f5f7fa',
scrollbarWidth: 'none', msOverflowStyle: 'none',
}}>
{renderTabContent()}
</Content>
</Layout>
</div>
);
}

View File

@@ -1,595 +0,0 @@
import { useState } from 'react';
import {
Card, Table, Button, Space, Form, Modal, Input, Select, InputNumber,
Tabs, Tag, Popconfirm, message, Divider, Switch, Checkbox
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined, ProductOutlined,
AppstoreOutlined, SwapOutlined, VideoCameraOutlined, AlertOutlined
} from '@ant-design/icons';
const { Option } = Select;
const { TabPane } = Tabs;
// ====================== 模拟基础数据 ======================
const productList = [
{
id: 1, name: '海康威视摄像头', code: 'hikvision', protocolType: 'onvif', manufacturer: '海康',
publishTopic: '/hik/device/upload', subscribeTopic: '/hik/device/cmd'
},
{
id: 2, name: '大华摄像头', code: 'dahua', protocolType: 'rtsp', manufacturer: '大华',
publishTopic: '/dahua/device/upload', subscribeTopic: '/dahua/device/cmd'
},
{
id: 3, name: '通用MQTT设备', code: 'mqtt_device', protocolType: 'mqtt', manufacturer: '通用',
publishTopic: '/iot/device/upload', subscribeTopic: '/iot/device/cmd'
},
];
const modelList = [
{ id: 10, name: '通用视频监控设备', code: 'video_camera', desc: '摄像头、云台、告警' },
{ id: 11, name: '环境监测设备', code: 'env_sensor', desc: '温湿度、烟感' },
{ id: 12, name: '电池供电设备', code: 'battery_device', desc: '电量、电压监测' },
];
// 物模型详情:属性增加 isAlarm(是否作为告警参数)
const modelDetailList: Record<number, {
props: any[];
events: any[];
actions: any[];
}> = {
10: {
props: [
{ id: 1, code: 'device_online', name: '在线状态', type: 'bool', rw: 'r', isAlarm: false },
{ id: 2, code: 'record_status', name: '录像状态', type: 'string', rw: 'r', isAlarm: false },
],
events: [
{ id: 1, code: 'alarm_perimeter', name: '周界入侵告警' },
],
actions: [
{ id: 1, code: 'take_snapshot', name: '截图' },
]
},
11: {
props: [
{ id: 1, code: 'temp', name: '温度', type: 'float', rw: 'r', isAlarm: true },
{ id: 2, code: 'humi', name: '湿度', type: 'float', rw: 'r', isAlarm: true },
],
events: [
{ id: 1, code: 'temp_over', name: '超温告警' },
],
actions: []
},
12: {
props: [
{ id: 1, code: 'battery', name: '电量', type: 'int', rw: 'r', isAlarm: true },
{ id: 2, code: 'voltage', name: '电压', type: 'float', rw: 'r', isAlarm: true },
],
events: [
{ id: 1, code: 'low_power', name: '低电量告警' },
],
actions: []
}
};
// 字段映射数据
const mappingInitList = [
{ id: 1, productId: 1, standardCode: 'device_online', sourceField: 'online_flag', convertRule: 'value==1?true:false' },
];
const deviceList = [
{
id: 1,
name: '1#环境传感器',
sn: 'SEN001',
productId: 3,
productName: '通用MQTT设备',
modelId: 11,
modelName: '环境监测设备',
protocolType: 'mqtt',
mqttServer: 'tcp://192.168.1.200',
mqttPort: 1883,
clientId: 'sen_001',
alarmConfig: {
tempMax: 60, tempMin: 0,
humiMax: 90, humiMin: 20
},
onlineStatus: 1,
createTime: '2026-05-07 10:23:45'
},
{
id: 2,
name: '1#电池设备',
sn: 'BAT001',
productId: 3,
productName: '通用MQTT设备',
modelId: 12,
modelName: '电池供电设备',
protocolType: 'mqtt',
mqttServer: 'tcp://192.168.1.200',
mqttPort: 1883,
clientId: 'bat_001',
alarmConfig: {
batteryMax: 100, batteryMin: 10,
voltageMax: 24, voltageMin: 12
},
onlineStatus: 1,
createTime: '2026-05-07 10:25:10'
}
];
const alarmList = [
{ id: 1, deviceId: 1, deviceName: '1#环境传感器', level: 'warn', content: '温度超过阈值 62℃', time: '2026-05-07 10:24:00' },
];
export default function DeviceAccessPage() {
const [activeTab, setActiveTab] = useState('device');
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [modalType, setModalType] = useState('');
const [editData, setEditData] = useState<any>(null);
const [protocol, setProtocol] = useState('onvif');
// 物模型详情
const [modelDetail, setModelDetail] = useState<any>({ props: [], events: [], actions: [] });
const [itemModal, setItemModal] = useState(false);
const [itemType, setItemType] = useState<'props' | 'events' | 'actions'>('props');
const [itemForm] = Form.useForm();
// 字段映射
const [mappingList, setMappingList] = useState<any[]>(mappingInitList);
const [mappingModal, setMappingModal] = useState(false);
const [mappingForm] = Form.useForm();
// 打开主弹窗
const openModal = (type: string, record?: any) => {
setModalType(type);
setEditData(record);
form.resetFields();
if (record) {
form.setFieldsValue(record);
if (type === 'model') {
setModelDetail(modelDetailList[record.id] || { props: [], events: [], actions: [] });
}
if (type === 'device') {
setProtocol(record.protocolType);
// 加载对应模型详情,用于动态生成告警项
setModelDetail(modelDetailList[record.modelId] || { props: [] });
}
} else {
setModelDetail({ props: [], events: [], actions: [] });
}
setVisible(true);
};
// 主表单保存
const handleSubmit = () => {
form.validateFields().then(() => {
message.success('保存成功');
setVisible(false);
});
};
// ========== 物模型 增删改 ==========
const openItemModal = (type: 'props' | 'events' | 'actions', record?: any) => {
setItemType(type);
itemForm.resetFields();
if (record) itemForm.setFieldsValue(record);
setItemModal(true);
};
const saveItem = () => {
itemForm.validateFields().then((val) => {
const list = [...modelDetail[itemType]];
if (val.id) {
const idx = list.findIndex(x => x.id === val.id);
idx >= 0 && (list[idx] = val);
} else {
list.push({ ...val, id: Date.now() });
}
setModelDetail({ ...modelDetail, [itemType]: list });
setItemModal(false);
message.success('保存成功');
});
};
const deleteItem = (type: 'props' | 'events' | 'actions', id: number) => {
const list = modelDetail[type].filter(x => x.id !== id);
setModelDetail({ ...modelDetail, [type]: list });
message.success('删除成功');
};
// ========== 字段映射 增删改 ==========
const openMappingModal = (record: any, row?: any) => {
setVisible(false);
mappingForm.resetFields();
if (row) {
mappingForm.setFieldsValue(row);
} else {
mappingForm.setFieldsValue({ productId: record.id });
}
setMappingModal(true);
};
const saveMapping = () => {
mappingForm.validateFields().then((val) => {
const list = [...mappingList];
if (val.id) {
const idx = list.findIndex(item => item.id === val.id);
list[idx] = val;
} else {
list.push({ ...val, id: Date.now() });
}
setMappingList(list);
setMappingModal(false);
setTimeout(() => setVisible(true), 300);
message.success('字段映射保存成功');
});
};
const delMapping = (id: number) => {
const list = mappingList.filter(item => item.id !== id);
setMappingList(list);
message.success('删除成功');
};
// 获取当前模型的告警属性(isAlarm=true)
const getAlarmProps = () => {
if (!modelDetail?.props) return [];
return modelDetail.props.filter(item => item.isAlarm);
};
// 表格列
const deviceColumns = [
{ title: '设备名称', dataIndex: 'name' },
{ title: 'SN', dataIndex: 'sn' },
{ title: '所属产品', dataIndex: 'productName' },
{ title: '物模型', dataIndex: 'modelName' },
{ title: '协议', dataIndex: 'protocolType', render: (t: string) => <Tag>{t.toUpperCase()}</Tag> },
{ title: '状态', dataIndex: 'onlineStatus', render: (s: number) => <Tag color={s === 1 ? 'green' : 'red'}>{s === 1 ? '在线' : '离线'}</Tag> },
{
title: '操作', render: (r: any) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('device', r)}>编辑</Button>
</Space>
)
}
];
const productColumns = [
{ title: '产品名称', dataIndex: 'name' },
{ title: '产品编码', dataIndex: 'code' },
{ title: '厂商', dataIndex: 'manufacturer' },
{ title: '协议类型', dataIndex: 'protocolType', render: (t: string) => <Tag>{t.toUpperCase()}</Tag> },
{ title: '上报Topic', dataIndex: 'publishTopic' },
{ title: '订阅Topic', dataIndex: 'subscribeTopic' },
{
title: '操作', render: (r: any) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openModal('product', r)}>编辑</Button>
<Button type="text" icon={<SwapOutlined />} onClick={() => openModal('mapping', r)}>字段映射</Button>
</Space>
)
}
];
const modelColumns = [
{ title: '模型名称', dataIndex: 'name' },
{ title: '模型编码', dataIndex: 'code' },
{ title: '描述', dataIndex: 'desc' },
{ title: '操作', render: (r: any) => <Button type="text" icon={<EditOutlined />} onClick={() => openModal('model', r)}>编辑</Button> }
];
const mappingColumns = [
{ title: '标准模型字段', dataIndex: 'standardCode' },
{ title: '厂商原始字段', dataIndex: 'sourceField' },
{ title: '转换规则', dataIndex: 'convertRule' },
{
title: '操作', render: (r: any) => (
<Space>
<Button type="text" icon={<EditOutlined />} onClick={() => openMappingModal(editData, r)}>编辑</Button>
<Popconfirm title="确定删除该映射?" onConfirm={() => delMapping(r.id)}>
<Button type="text" danger icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
</Space>
)
}
];
const alarmColumns = [
{ title: '设备名称', dataIndex: 'deviceName' },
{ title: '告警级别', dataIndex: 'level' },
{ title: '告警内容', dataIndex: 'content' },
{ title: '告警时间', dataIndex: 'time' },
];
const propColumns = [
{ title: '字段编码', dataIndex: 'code' },
{ title: '字段名称', dataIndex: 'name' },
{ title: '数据类型', dataIndex: 'type' },
{ title: '读写类型', dataIndex: 'rw' },
{
title: '告警参数', render: (r: any) => (
<Checkbox checked={r.isAlarm} disabled>启用</Checkbox>
)
},
{
title: '操作', render: (r: any) => (
<Space>
<Button type="text" onClick={() => openItemModal('props', r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => deleteItem('props', r.id)}>
<Button type="text" danger>删除</Button>
</Popconfirm>
</Space>
)
}
];
const eventActionColumns = [
{ title: '编码', dataIndex: 'code' },
{ title: '名称', dataIndex: 'name' },
{
title: '操作', render: (r: any) => (
<Space>
<Button type="text" onClick={() => openItemModal(itemType, r)}>编辑</Button>
<Popconfirm title="确定删除?" onConfirm={() => deleteItem(itemType, r.id)}>
<Button type="text" danger>删除</Button>
</Popconfirm>
</Space>
)
}
];
return (
<div className="bg-[#f5f7fa] min-h-screen p-5">
<Card className="mb-4">
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={<span><VideoCameraOutlined />设备管理</span>} key="device" />
<TabPane tab={<span><ProductOutlined />产品管理</span>} key="product" />
<TabPane tab={<span><AppstoreOutlined />物模型管理</span>} key="model" />
<TabPane tab={<span><AlertOutlined />告警列表</span>} key="alarm" />
</Tabs>
</Card>
{/* 设备管理 */}
{activeTab === 'device' && (
<Card>
<Space className="mb-3">
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('device')}>新增设备接入</Button>
</Space>
<Table rowKey="id" columns={deviceColumns} dataSource={deviceList} pagination={{ pageSize: 10 }} />
</Card>
)}
{/* 产品管理 */}
{activeTab === 'product' && (
<Card>
<Space className="mb-3">
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('product')}>新增产品</Button>
</Space>
<Table rowKey="id" columns={productColumns} dataSource={productList} pagination={{ pageSize: 10 }} />
</Card>
)}
{/* 物模型管理 */}
{activeTab === 'model' && (
<Card>
<Space className="mb-3">
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal('model')}>新增物模型</Button>
</Space>
<Table rowKey="id" columns={modelColumns} dataSource={modelList} pagination={{ pageSize: 10 }} />
</Card>
)}
{/* 告警列表 */}
{activeTab === 'alarm' && (
<Card>
<Table rowKey="id" columns={alarmColumns} dataSource={alarmList} pagination={{ pageSize: 10 }} />
</Card>
)}
{/* 主弹窗:设备/产品/物模型 */}
<Modal
open={visible}
title={
modalType === 'device' ? '设备接入配置' :
modalType === 'product' ? '产品配置' :
modalType === 'model' ? '物模型配置' :
modalType === 'mapping' ? '字段映射管理' : ''
}
onCancel={() => setVisible(false)}
onOk={handleSubmit}
width={800}
destroyOnClose
>
<Form form={form} layout="vertical">
{/* 设备接入 */}
{modalType === 'device' && (
<Tabs defaultActiveKey="base">
<TabPane tab="基础信息" key="base">
<Form.Item label="设备名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="设备SN" name="sn" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="关联产品" name="productId" rules={[{ required: true }]}>
<Select onChange={(_, o: any) => setProtocol(o.protocolType)}>
{productList.map(p => <Option key={p.id} value={p.id}>{p.name}</Option>)}
</Select>
</Form.Item>
<Form.Item label="关联物模型" name="modelId" rules={[{ required: true }]}>
<Select onChange={(val) => setModelDetail(modelDetailList[val] || { props: [] })}>
{modelList.map(m => <Option key={m.id} value={m.id}>{m.name}</Option>)}
</Select>
</Form.Item>
</TabPane>
<TabPane tab="协议配置" key="protocol">
{protocol === 'onvif' && (
<>
<Form.Item label="设备IP" name="ip"><Input /></Form.Item>
<Form.Item label="端口" name="port"><InputNumber className="w-full" /></Form.Item>
<Form.Item label="登录账号" name="username"><Input /></Form.Item>
<Form.Item label="登录密码" name="password"><Input.Password /></Form.Item>
</>
)}
{protocol === 'rtsp' && (
<Form.Item label="RTSP流地址" name="rtspUrl"><Input placeholder="rtsp://xxx" /></Form.Item>
)}
{protocol === 'mqtt' && (
<>
<Card size="small" title="MQTT连接配置" className="mb-2">
<Form.Item label="MQTT服务器" name="mqttServer"><Input placeholder="tcp://192.168.1.200" /></Form.Item>
<Form.Item label="端口" name="mqttPort"><InputNumber className="w-full" defaultValue={1883} /></Form.Item>
<Form.Item label="ClientID" name="clientId"><Input placeholder="设备唯一标识" /></Form.Item>
<Form.Item label="用户名" name="mqttUsername"><Input /></Form.Item>
<Form.Item label="密码" name="mqttPassword"><Input.Password /></Form.Item>
</Card>
<Card size="small" title="MQTT Topic配置(继承自产品)">
<div className="text-sm">上报主题:{editData?.publishTopic || '未配置'}</div>
<div className="text-sm">订阅主题:{editData?.subscribeTopic || '未配置'}</div>
</Card>
</>
)}
</TabPane>
{/* 报警阈值:动态渲染当前模型的告警属性 */}
<TabPane tab="报警阈值" key="alarmThreshold">
<div className="text-sm mb-2">当前物模型:{editData?.modelName},仅显示告警参数</div>
{getAlarmProps().map((item: any) => (
<div key={item.code}>
<Form.Item label={`${item.name} 上限`} name={['alarmConfig', `${item.code}Max`]}>
<InputNumber className="w-full" placeholder={`单位:${item.type === 'float' ? '数值' : '整数'}`} />
</Form.Item>
<Form.Item label={`${item.name} 下限`} name={['alarmConfig', `${item.code}Min`]}>
<InputNumber className="w-full" />
</Form.Item>
</div>
))}
{getAlarmProps().length === 0 && <Empty description="该物模型未配置告警参数" />}
</TabPane>
</Tabs>
)}
{/* 产品配置 + MQTT Topic */}
{modalType === 'product' && (
<>
<Form.Item label="产品名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="产品编码" name="code" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="协议类型" name="protocolType" rules={[{ required: true }]}>
<Select>
<Option value="onvif">ONVIF</Option>
<Option value="rtsp">RTSP</Option>
<Option value="mqtt">MQTT</Option>
</Select>
</Form.Item>
<Form.Item label="厂商名称" name="manufacturer"><Input /></Form.Item>
<Divider>厂商级 MQTT Topic 配置</Divider>
<Form.Item label="设备上报主题(Publish)" name="publishTopic"><Input placeholder="/iot/device/upload" /></Form.Item>
<Form.Item label="平台下发主题(Subscribe)" name="subscribeTopic"><Input placeholder="/iot/device/cmd" /></Form.Item>
</>
)}
{/* 物模型完整编辑(含告警参数勾选) */}
{modalType === 'model' && (
<>
<Form.Item label="模型名称" name="name" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="模型编码" name="code" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item label="模型描述" name="desc"><Input.TextArea rows={2} /></Form.Item>
<Divider orientation="left">属性列表(可勾选作为告警参数)</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('props')} className="mb-2">新增属性</Button>
<Table size="small" rowKey="id" columns={propColumns} dataSource={modelDetail.props} pagination={false} />
<Divider orientation="left">事件列表</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('events')} className="mb-2">新增事件</Button>
<Table size="small" rowKey="id" columns={eventActionColumns} dataSource={modelDetail.events} pagination={false} />
<Divider orientation="left">服务/指令</Divider>
<Button size="small" type="primary" icon={<PlusOutlined />} onClick={() => openItemModal('actions')} className="mb-2">新增指令</Button>
<Table size="small" rowKey="id" columns={eventActionColumns} dataSource={modelDetail.actions} pagination={false} />
</>
)}
</Form>
{/* 字段映射列表 */}
{modalType === 'mapping' && (
<div className="mt-4">
<Divider orientation="left">{editData?.name} - 字段映射配置</Divider>
<Space className="mb-3">
<Button type="primary" size="small" icon={<PlusOutlined />} onClick={() => openMappingModal(editData)}>新增映射</Button>
</Space>
<Table size="small" rowKey="id" columns={mappingColumns} dataSource={mappingList.filter(item => item.productId === editData?.id)} pagination={false} />
</div>
)}
</Modal>
{/* 物模型子项编辑弹窗(属性增加isAlarm) */}
<Modal
open={itemModal}
title={itemType === 'props' ? '属性配置' : itemType === 'events' ? '事件配置' : '指令配置'}
onCancel={() => setItemModal(false)}
onOk={saveItem}
width={520}
>
<Form form={itemForm} layout="vertical">
<Form.Item name="id" hidden><Input /></Form.Item>
<Form.Item label="字段编码" name="code" rules={[{ required: true }]}><Input placeholder="如:temp" /></Form.Item>
<Form.Item label="字段名称" name="name" rules={[{ required: true }]}><Input placeholder="如:温度" /></Form.Item>
{itemType === 'props' && (
<>
<Form.Item label="数据类型" name="type" rules={[{ required: true }]}>
<Select>
<Option value="bool">布尔 bool</Option>
<Option value="int">数字 int</Option>
<Option value="float">浮点 float</Option>
<Option value="string">字符串 string</Option>
</Select>
</Form.Item>
<Form.Item label="读写权限" name="rw" rules={[{ required: true }]}>
<Select>
<Option value="r">只读</Option>
<Option value="rw">读写</Option>
</Select>
</Form.Item>
<Form.Item label="作为告警参数" name="isAlarm" valuePropName="checked">
<Switch />
</Form.Item>
</>
)}
</Form>
</Modal>
{/* 字段映射弹窗 */}
<Modal
open={mappingModal}
title="字段映射配置"
onCancel={() => setMappingModal(false)}
onOk={saveMapping}
width={600}
destroyOnClose
>
<Form form={mappingForm} layout="vertical">
<Form.Item name="id" hidden><Input /></Form.Item>
<Form.Item name="productId" hidden><Input /></Form.Item>
<Form.Item label="平台标准字段" name="standardCode" rules={[{ required: true }]}>
<Select placeholder="请选择标准字段">
{modelDetail.props?.map((item: any) => (
<Option key={item.code} value={item.code}>{item.name}({item.code})</Option>
))}
</Select>
</Form.Item>
<Form.Item label="厂商原始字段" name="sourceField" rules={[{ required: true }]}>
<Input placeholder="例如:temperature" />
</Form.Item>
<Form.Item label="转换规则" name="convertRule">
<Input placeholder="例如:value>50" />
</Form.Item>
</Form>
</Modal>
</div>
);
}

View File

@@ -334,7 +334,8 @@ export default function Home() {
extra={<Button type="link" size="small">更多&gt;</Button>}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<Progress percent={58} type="circle" size={80} strokeColor="#52c41a" successPercent={25} />
<Progress percent={58} type="circle" size={80} strokeColor="#52c41a" success={{ percent: 25 }} // ✅ 新写法
/>
<div style={{ fontSize: 12 }}>
<div><span style={{ color: '#52c41a' }}>●</span> 已完成 7 (58%)</div>
<div><span style={{ color: '#faad14' }}>●</span> 进行中 3 (25%)</div>

View File

@@ -11,12 +11,12 @@ import {
ExperimentOutlined,
EnvironmentOutlined
} from '@ant-design/icons';
import UserRolePage from './UserPage';
import MenuList from './MenuList';
import Organization from './Organization';
import StationManage from './StationManage';
import UserRolePage from '../components/SystemSetting/UserPage';
import MenuList from '../components/SystemSetting/MenuList';
import Organization from '../components/SystemSetting/Organization';
import StationManage from '../components/SystemSetting/StationManage';
import { getMenuList } from '../api/mune';
import DeviceAccessPage from './DeviceAccessPage';
import DeviceAccessPage from '../components/SystemSetting/DeviceAccessPage';
const { Header, Content } = Layout;
const { Text } = Typography;

View File

@@ -14,9 +14,9 @@ const Alerts = lazy(() => import('../pages/Alerts'));
const Downloads = lazy(() => import('../pages/Downloads'));
const Roles = lazy(() => import('../pages/Roles'));
const Users = lazy(() => import('../pages/Users'));
const MenuList = lazy(() => import('../pages/MenuList'));
const Organization = lazy(() => import('../pages/Organization'));
const StationManage = lazy(() => import('../pages/StationManage'));
const MenuList = lazy(() => import('../components/SystemSetting/MenuList'));
const Organization = lazy(() => import('../components/SystemSetting/Organization'));
const StationManage = lazy(() => import('../components/SystemSetting/StationManage'));
const SerialNumGenerate = lazy(() => import('../pages/SerialNumGenerate'));
const SystemSetting = lazy(() => import('../pages/SystemSetting'));