告警中心 初始化
This commit is contained in:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
18
src/api/deviceAccess.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '../request';
|
||||
|
||||
//新增物模型
|
||||
export function addDeviceModel(data) {
|
||||
return request({
|
||||
url: 'system/devicemodel/add',
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
349
src/components/SystemSetting/DeviceAccessPage.tsx
Normal file
349
src/components/SystemSetting/DeviceAccessPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
183
src/components/alerts/AlarmHistory.tsx
Normal file
183
src/components/alerts/AlarmHistory.tsx
Normal 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;
|
||||
21
src/components/alerts/AlarmOverview.tsx
Normal file
21
src/components/alerts/AlarmOverview.tsx
Normal 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;
|
||||
138
src/components/alerts/AlarmRealtime.tsx
Normal file
138
src/components/alerts/AlarmRealtime.tsx
Normal 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;
|
||||
181
src/components/alerts/AlarmRules.tsx
Normal file
181
src/components/alerts/AlarmRules.tsx
Normal 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;
|
||||
129
src/components/alerts/AlarmStatistics.tsx
Normal file
129
src/components/alerts/AlarmStatistics.tsx
Normal 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;
|
||||
168
src/components/alerts/AlarmSubscription.tsx
Normal file
168
src/components/alerts/AlarmSubscription.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -334,7 +334,8 @@ export default function Home() {
|
||||
extra={<Button type="link" size="small">更多></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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user