优化设备管理界面讲将5个子页面抽离出组件 轻量化 也优化了 两个组件内弹窗的数据影响 优化了 设备参数弹窗的 loading效果
This commit is contained in:
150
src/components/SystemSetting/AllDeviceManagement.tsx
Normal file
150
src/components/SystemSetting/AllDeviceManagement.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Form,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
App,
|
||||
} from 'antd';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const allDeviceList = [
|
||||
{ id: 1, code: 'CAM-01', name: '摄像头 CAM-01', type: 'CAMERA', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 2, code: 'CAM-02', name: '摄像头 CAM-02', type: 'CAMERA', online: 'OFFLINE', status: 'FAULT' },
|
||||
{ id: 3, code: 'CAM-03', name: '摄像头 CAM-03', type: 'CAMERA', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 4, code: 'CLEAN-01', name: '清洗机器人 CLEAN-01', type: 'CLEANING_DEVICE', online: 'ONLINE', status: 'IDLE' },
|
||||
{ id: 5, code: 'CMB-A01', name: '汇流箱 CMB-A01', type: 'COMBINER_BOX', online: 'ONLINE', status: 'WARNING' },
|
||||
{ id: 6, code: 'CMB-A02', name: '汇流箱 CMB-A02', type: 'COMBINER_BOX', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 7, code: 'INV-A01', name: '逆变器 INV-A01', type: 'INVERTER', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 8, code: 'INV-A02', name: '逆变器 INV-A02', type: 'INVERTER', online: 'ONLINE', status: 'WARNING' },
|
||||
{ id: 9, code: 'INV-B01', name: '逆变器 INV-B01', type: 'INVERTER', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 10, code: 'METER-01', name: '电表 METER-01', type: 'METER', online: 'ONLINE', status: 'NORMAL' },
|
||||
{ id: 11, code: 'ROBOT-01', name: '运维机器人 ROBOT-01', type: 'ROBOT', online: 'ONLINE', status: 'IDLE' },
|
||||
{ id: 12, code: 'DRONE-01', name: '无人机 DRONE-01', type: 'DRONE', online: 'OFFLINE', status: 'FAULT' },
|
||||
{ id: 13, code: 'WEATHER-01', name: '气象站 WEATHER-01', type: 'WEATHER_STATION', online: 'ONLINE', status: 'NORMAL' },
|
||||
];
|
||||
|
||||
export default function AllDeviceManagement() {
|
||||
const { modal } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deviceList, setDeviceList] = useState(allDeviceList);
|
||||
|
||||
const columns = [
|
||||
{ title: '设备编码', dataIndex: 'code', key: 'code' },
|
||||
{ title: '设备名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'type', key: 'type' },
|
||||
{
|
||||
title: '在线', dataIndex: 'online', key: 'online',
|
||||
render: (text) => (
|
||||
<Tag color={text === 'ONLINE' ? 'green' : 'red'}>{text}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '运行', dataIndex: 'status', key: 'status',
|
||||
render: (text) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NORMAL: 'green',
|
||||
FAULT: 'red',
|
||||
WARNING: 'orange',
|
||||
IDLE: 'blue',
|
||||
};
|
||||
return <Tag color={colorMap[text] || 'default'}>{text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action',
|
||||
render: (_, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>删除</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = () => {
|
||||
form.validateFields().then(values => {
|
||||
const keyword = values.keyword?.toLowerCase() || '';
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
const filtered = allDeviceList.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) || item.code.toLowerCase().includes(keyword)
|
||||
);
|
||||
setDeviceList(filtered);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
setDeviceList(allDeviceList);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
App.useApp();
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
modal.info({
|
||||
title: '编辑设备',
|
||||
content: `编辑设备:${record.name}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (record) => {
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除设备 ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
setDeviceList(prev => prev.filter((item: any) => item.id !== record.id));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Form form={form} layout="inline">
|
||||
<Form.Item name="keyword">
|
||||
<Input placeholder="请输入设备名称" style={{ width: 220 }} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={deviceList}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: deviceList.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
449
src/components/SystemSetting/CameraManagement.tsx
Normal file
449
src/components/SystemSetting/CameraManagement.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Form,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
App,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
InputNumber,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
VideoCameraOutlined,
|
||||
EnvironmentOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const mockCameraList = [
|
||||
{
|
||||
id: 1,
|
||||
code: 'CAM-01',
|
||||
name: '东门入口摄像头',
|
||||
ip: '192.168.1.101',
|
||||
online: 'ONLINE',
|
||||
status: 'NORMAL',
|
||||
location: '园区东门',
|
||||
type: '高清球机',
|
||||
manufacturer: '海康威视',
|
||||
resolution: '4K',
|
||||
ptz: true,
|
||||
nightVision: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'CAM-02',
|
||||
name: '西门入口摄像头',
|
||||
ip: '192.168.1.102',
|
||||
online: 'OFFLINE',
|
||||
status: 'FAULT',
|
||||
location: '园区西门',
|
||||
type: '高清枪机',
|
||||
manufacturer: '大华',
|
||||
resolution: '1080P',
|
||||
ptz: false,
|
||||
nightVision: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
code: 'CAM-03',
|
||||
name: '停车场摄像头',
|
||||
ip: '192.168.1.103',
|
||||
online: 'ONLINE',
|
||||
status: 'NORMAL',
|
||||
location: '地下停车场B1',
|
||||
type: '高清球机',
|
||||
manufacturer: '海康威视',
|
||||
resolution: '1080P',
|
||||
ptz: true,
|
||||
nightVision: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
code: 'CAM-04',
|
||||
name: '办公楼大堂摄像头',
|
||||
ip: '192.168.1.104',
|
||||
online: 'ONLINE',
|
||||
status: 'WARNING',
|
||||
location: '办公楼1层大堂',
|
||||
type: '半球摄像机',
|
||||
manufacturer: '宇视',
|
||||
resolution: '4K',
|
||||
ptz: false,
|
||||
nightVision: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
code: 'CAM-05',
|
||||
name: '仓库监控摄像头',
|
||||
ip: '192.168.1.105',
|
||||
online: 'ONLINE',
|
||||
status: 'NORMAL',
|
||||
location: '物料仓库',
|
||||
type: '高清枪机',
|
||||
manufacturer: '海康威视',
|
||||
resolution: '4K',
|
||||
ptz: false,
|
||||
nightVision: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CameraManagement() {
|
||||
const { modal } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cameraList, setCameraList] = useState(mockCameraList);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editForm] = Form.useForm();
|
||||
const [currentCamera, setCurrentCamera] = useState<any>(null);
|
||||
|
||||
const columns = [
|
||||
{ title: '设备编码', dataIndex: 'code', key: 'code', width: 120 },
|
||||
{ title: '设备名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 140 },
|
||||
{ title: '摄像头类型', dataIndex: 'type', key: 'type', width: 120 },
|
||||
{ title: '品牌', dataIndex: 'manufacturer', key: 'manufacturer', width: 120 },
|
||||
{ title: '分辨率', dataIndex: 'resolution', key: 'resolution', width: 100 },
|
||||
{
|
||||
title: '位置', dataIndex: 'location', key: 'location',
|
||||
render: (text) => (
|
||||
<Space>
|
||||
<EnvironmentOutlined style={{ color: '#1890ff' }} />
|
||||
{text}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '云台', dataIndex: 'ptz', key: 'ptz', width: 80,
|
||||
render: (ptz) => ptz ? <Tag color="blue">支持</Tag> : <Tag color="default">不支持</Tag>
|
||||
},
|
||||
{
|
||||
title: '夜视', dataIndex: 'nightVision', key: 'nightVision', width: 80,
|
||||
render: (nv) => nv ? <Tag color="green">支持</Tag> : <Tag color="default">不支持</Tag>
|
||||
},
|
||||
{
|
||||
title: '在线', dataIndex: 'online', key: 'online', width: 100,
|
||||
render: (text) => (
|
||||
<Tag color={text === 'ONLINE' ? 'green' : 'red'}>{text === 'ONLINE' ? '在线' : '离线'}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
||||
render: (text) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
NORMAL: 'green',
|
||||
FAULT: 'red',
|
||||
WARNING: 'orange',
|
||||
};
|
||||
const textMap: Record<string, string> = {
|
||||
NORMAL: '正常',
|
||||
FAULT: '故障',
|
||||
WARNING: '警告',
|
||||
};
|
||||
return <Tag color={colorMap[text] || 'default'}>{textMap[text] || text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 200, fixed: 'right' as const,
|
||||
render: (_, record: any) => (
|
||||
<Space>
|
||||
<Button type="link" icon={<VideoCameraOutlined />} onClick={() => handleView(record)}>预览</Button>
|
||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record)}>删除</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSearch = () => {
|
||||
form.validateFields().then(values => {
|
||||
const keyword = values.keyword?.toLowerCase() || '';
|
||||
const status = values.status;
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
let filtered = mockCameraList.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.code.toLowerCase().includes(keyword) ||
|
||||
item.ip.includes(keyword)
|
||||
);
|
||||
if (status) {
|
||||
filtered = filtered.filter(item => item.status === status);
|
||||
}
|
||||
setCameraList(filtered);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
setCameraList(mockCameraList);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setCurrentCamera(null);
|
||||
editForm.resetFields();
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleView = (record) => {
|
||||
modal.info({
|
||||
title: `${record.name} - 视频预览`,
|
||||
width: 720,
|
||||
content: (
|
||||
<div style={{
|
||||
height: 400,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: 18
|
||||
}}>
|
||||
<Space direction="vertical" align="center">
|
||||
<VideoCameraOutlined style={{ fontSize: 48 }} />
|
||||
<span>视频预览占位 - {record.name}</span>
|
||||
<span style={{ fontSize: 14, opacity: 0.8 }}>IP: {record.ip}</span>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (record) => {
|
||||
setCurrentCamera(record);
|
||||
editForm.setFieldsValue(record);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = (record) => {
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除摄像头 ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
setCameraList(prev => prev.filter((item: any) => item.id !== record.id));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = (values) => {
|
||||
if (currentCamera) {
|
||||
setCameraList(prev => prev.map((item: any) =>
|
||||
item.id === currentCamera.id ? { ...item, ...values } : item
|
||||
));
|
||||
} else {
|
||||
const newId = Math.max(...cameraList.map((i: any) => i.id)) + 1;
|
||||
setCameraList(prev => [...prev, { id: newId, ...values, online: 'ONLINE', status: 'NORMAL' }]);
|
||||
}
|
||||
setEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} md={8}>
|
||||
<Card size="small">
|
||||
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ color: '#86909C', fontSize: 12 }}>摄像头总数</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700 }}>{cameraList.length}</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: 'rgba(24, 144, 255, 0.1)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#1890ff',
|
||||
fontSize: 24
|
||||
}}>
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card size="small">
|
||||
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ color: '#86909C', fontSize: 12 }}>在线</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#52c41a' }}>
|
||||
{cameraList.filter((c: any) => c.online === 'ONLINE').length}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: 'rgba(82, 196, 26, 0.1)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#52c41a',
|
||||
fontSize: 24
|
||||
}}>
|
||||
✔
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} md={8}>
|
||||
<Card size="small">
|
||||
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ color: '#86909C', fontSize: 12 }}>离线/故障</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#f5222d' }}>
|
||||
{cameraList.filter((c: any) => c.online === 'OFFLINE' || c.status === 'FAULT').length}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
background: 'rgba(245, 34, 45, 0.1)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#f5222d',
|
||||
fontSize: 24
|
||||
}}>
|
||||
⚠
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Form form={form} layout="inline">
|
||||
<Form.Item name="keyword">
|
||||
<Input placeholder="请输入名称/编码/IP" style={{ width: 240 }} prefix={<SearchOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status">
|
||||
<Select placeholder="状态筛选" style={{ width: 140 }} allowClear>
|
||||
<Select.Option value="NORMAL">正常</Select.Option>
|
||||
<Select.Option value="WARNING">警告</Select.Option>
|
||||
<Select.Option value="FAULT">故障</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" onClick={handleSearch}>查询</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增摄像头</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={cameraList}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: cameraList.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1400 }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={currentCamera ? '编辑摄像头' : '新增摄像头'}
|
||||
open={editModalVisible}
|
||||
onCancel={() => {
|
||||
setEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
}}
|
||||
onOk={() => editForm.submit()}
|
||||
width={600}
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleSave}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="设备编码" name="code" rules={[{ required: true, message: '请输入设备编码' }]}>
|
||||
<Input placeholder="请输入设备编码" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="设备名称" name="name" rules={[{ required: true, message: '请输入设备名称' }]}>
|
||||
<Input placeholder="请输入设备名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="IP地址" name="ip" rules={[{ required: true, message: '请输入IP地址' }]}>
|
||||
<Input placeholder="例如: 192.168.1.100" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="摄像头类型" name="type" rules={[{ required: true }]}>
|
||||
<Select placeholder="请选择类型">
|
||||
<Select.Option value="高清球机">高清球机</Select.Option>
|
||||
<Select.Option value="高清枪机">高清枪机</Select.Option>
|
||||
<Select.Option value="半球摄像机">半球摄像机</Select.Option>
|
||||
<Select.Option value="全景摄像机">全景摄像机</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="品牌" name="manufacturer">
|
||||
<Select placeholder="请选择品牌">
|
||||
<Select.Option value="海康威视">海康威视</Select.Option>
|
||||
<Select.Option value="大华">大华</Select.Option>
|
||||
<Select.Option value="宇视">宇视</Select.Option>
|
||||
<Select.Option value="其他">其他</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="分辨率" name="resolution">
|
||||
<Select placeholder="请选择分辨率">
|
||||
<Select.Option value="720P">720P</Select.Option>
|
||||
<Select.Option value="1080P">1080P</Select.Option>
|
||||
<Select.Option value="4K">4K</Select.Option>
|
||||
<Select.Option value="8K">8K</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item label="安装位置" name="location">
|
||||
<Input placeholder="请输入安装位置" prefix={<EnvironmentOutlined />} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="云台控制" name="ptz" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="夜视功能" name="nightVision" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
456
src/components/SystemSetting/DroneManagement.tsx
Normal file
456
src/components/SystemSetting/DroneManagement.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Form,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Modal,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Typography,
|
||||
App,
|
||||
} from 'antd';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
ReloadOutlined,
|
||||
LinkOutlined,
|
||||
DisconnectOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
getPlaneDeviceList,
|
||||
bindDrone,
|
||||
unbindDrone,
|
||||
} from '@/api/device';
|
||||
import { getOrgList } from '@/api/organization/index';
|
||||
import { getSiteByOrgId, getStationList } from '@/api/stationManage/index';
|
||||
import { getUser } from '@/api/user/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/src/store';
|
||||
import utils from '@/lib/utils';
|
||||
|
||||
interface DroneManagementProps {
|
||||
allOrgOptions: any[];
|
||||
allSiteOptions: any[];
|
||||
allUserOptions: any[];
|
||||
onAllDataNeedRefresh?: () => void;
|
||||
}
|
||||
|
||||
export default function DroneManagement({
|
||||
allOrgOptions,
|
||||
allSiteOptions,
|
||||
allUserOptions,
|
||||
onAllDataNeedRefresh,
|
||||
}: DroneManagementProps) {
|
||||
const { modal } = App.useApp();
|
||||
const { userInfo } = useSelector((state: RootState) => state.user);
|
||||
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [droneList, setDroneList] = useState([]);
|
||||
const [bindModalVisible, setBindModalVisible] = useState(false);
|
||||
const [bindForm] = Form.useForm();
|
||||
const [selectedDroneKeys, setSelectedDroneKeys] = useState<any[]>([]);
|
||||
|
||||
const [droneParams, setDroneParams] = useState({
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
sn: '',
|
||||
status: ''
|
||||
});
|
||||
const [droneTotal, setDroneTotal] = useState(0);
|
||||
|
||||
const [droneOrgOptions, setDroneOrgOptions] = useState([]);
|
||||
const [droneSiteOptions, setDroneSiteOptions] = useState([]);
|
||||
const [droneUserOptions, setDroneUserOptions] = useState([]);
|
||||
|
||||
const isAdmin = userInfo?.roleKey === 'admin';
|
||||
const isManager = userInfo?.roleKey === 'manager' || userInfo?.roleKey === 'Manager';
|
||||
const orgFieldDisabled = !isAdmin;
|
||||
const siteFieldDisabled = !(isAdmin || isManager);
|
||||
const userFieldDisabled = false;
|
||||
|
||||
const defaultOrgId = currentStation?.orgId || userInfo?.orgId;
|
||||
const defaultSiteId = stationId || currentStation?.siteId || currentStation?.id;
|
||||
const defaultUserId = userInfo?.userId;
|
||||
|
||||
const fetchDroneList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getPlaneDeviceList(droneParams);
|
||||
if (res.code === 200) {
|
||||
setDroneList(res.rows || []);
|
||||
setDroneTotal(res.total || res.rows?.length || 0);
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('获取无人机列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOrgs = async () => {
|
||||
try {
|
||||
const res = await getOrgList({});
|
||||
if (res.code === 200) {
|
||||
setDroneOrgOptions(res.rows || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error('获取组织列表失败', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSites = async (orgId) => {
|
||||
try {
|
||||
const res = await getSiteByOrgId({ id: orgId });
|
||||
if (res.code === 200) {
|
||||
setDroneSiteOptions(res.data || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsers = async (siteId) => {
|
||||
try {
|
||||
const res = await getUser({
|
||||
siteId,
|
||||
pageNum: 1,
|
||||
pageSize: 1000
|
||||
});
|
||||
if (res.code === 200) {
|
||||
setDroneUserOptions(res.rows || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchDroneList();
|
||||
}, [droneParams]);
|
||||
|
||||
const openBindModal = async () => {
|
||||
if (!selectedDroneKeys.length) {
|
||||
return utils.message.warning('请先在列表中选中需要绑定的无人机');
|
||||
}
|
||||
bindForm.resetFields();
|
||||
setBindModalVisible(true);
|
||||
setDroneOrgOptions([]);
|
||||
setDroneSiteOptions([]);
|
||||
setDroneUserOptions([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!bindModalVisible) {
|
||||
bindForm.resetFields();
|
||||
return;
|
||||
}
|
||||
const fetchAndSetForm = async () => {
|
||||
const orgRes = await fetchOrgs();
|
||||
const orgList = orgRes?.code === 200 ? (orgRes?.rows || []) : [];
|
||||
setDroneOrgOptions(orgList);
|
||||
|
||||
const finalOrgId = defaultOrgId && orgList.some(o => (o.id || o.orgId) == defaultOrgId)
|
||||
? defaultOrgId
|
||||
: (orgList[0]?.id || orgList[0]?.orgId);
|
||||
|
||||
let siteList = [];
|
||||
if (finalOrgId) {
|
||||
const siteRes = await fetchSites(finalOrgId);
|
||||
siteList = siteRes?.code === 200 ? (siteRes?.data || []) : [];
|
||||
}
|
||||
setDroneSiteOptions(siteList);
|
||||
|
||||
const finalSiteId = defaultSiteId && siteList.some(s => (s.id || s.siteId) == defaultSiteId)
|
||||
? defaultSiteId
|
||||
: (siteList[0]?.id || siteList[0]?.siteId);
|
||||
|
||||
let userList = [];
|
||||
if (finalSiteId) {
|
||||
const userRes = await fetchUsers(finalSiteId);
|
||||
userList = userRes?.code === 200 ? (userRes?.rows || []) : [];
|
||||
}
|
||||
setDroneUserOptions(userList);
|
||||
|
||||
const finalUserId = defaultUserId && userList.some(u => u.userId == defaultUserId)
|
||||
? defaultUserId
|
||||
: (userList[0]?.userId);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
bindForm.setFieldsValue({
|
||||
sns: selectedDroneKeys.join(', '),
|
||||
orgId: finalOrgId,
|
||||
siteId: finalSiteId,
|
||||
userId: finalUserId,
|
||||
});
|
||||
});
|
||||
};
|
||||
fetchAndSetForm();
|
||||
}, [bindModalVisible, defaultOrgId, defaultSiteId, defaultUserId, selectedDroneKeys]);
|
||||
|
||||
const handleBind = async (values) => {
|
||||
try {
|
||||
const payload = {
|
||||
siteId: values.siteId,
|
||||
orgId: values.orgId,
|
||||
userId: values.userId,
|
||||
sns: values.sns.split(',').map(s => s.trim()),
|
||||
};
|
||||
const res = await bindDrone(payload);
|
||||
if (res.code === 200 && res.data == true) {
|
||||
utils.message.success('绑定成功');
|
||||
setBindModalVisible(false);
|
||||
setSelectedDroneKeys([]);
|
||||
bindForm.resetFields();
|
||||
fetchDroneList();
|
||||
onAllDataNeedRefresh?.();
|
||||
} else {
|
||||
utils.message.error(res.msg || '绑定失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnbind = (sns, type) => {
|
||||
const validSns = (sns || []).filter(item =>
|
||||
item !== null && item !== undefined && item !== '' && item.trim() !== ''
|
||||
);
|
||||
if (validSns.length === 0) {
|
||||
return utils.message.warning('未选择有效设备,无法解绑');
|
||||
}
|
||||
const typeText = { 1: '人员', 2: '场站', 3: '组织' }[type];
|
||||
modal.confirm({
|
||||
title: '确认解绑',
|
||||
content: `确定要为选中的无人机清除${typeText}绑定吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await unbindDrone({ sns: validSns, type });
|
||||
if (res.code === 200) {
|
||||
utils.message.success('解绑成功');
|
||||
fetchDroneList();
|
||||
setSelectedDroneKeys([]);
|
||||
onAllDataNeedRefresh?.();
|
||||
} else {
|
||||
utils.message.error(res.msg || '解绑失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const droneColumns = [
|
||||
{ title: '设备名称', width: 150, dataIndex: 'drone_callsign', key: 'name' },
|
||||
{ title: 'SN码', width: 180, dataIndex: 'device_sn', key: 'sn' },
|
||||
{ title: '机场SN', width: 150, dataIndex: 'gateway_sn', key: 'gsn' },
|
||||
{
|
||||
title: '所属组织', width: 150, key: 'orgName',
|
||||
render: (_, record) => {
|
||||
const org = allOrgOptions.find(o => (o.orgId || o.id) == record.orgId);
|
||||
return org ? (org.orgName || org.name) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属场站', width: 150, key: 'siteName',
|
||||
render: (_, record) => {
|
||||
const site = allSiteOptions.find(s => (s.siteId || s.id) == record.siteId);
|
||||
return site ? (site.siteName || site.name) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人', width: 150, key: 'userName',
|
||||
render: (_, record) => {
|
||||
const user = allUserOptions.find(u => u.userId == record.userId);
|
||||
return user ? (user.nickName || user.userName) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态', width: 150, dataIndex: 'onlineStatus', key: 'status',
|
||||
render: s => <Tag color={s === 1 ? 'green' : 'default'}>{s === 1 ? '在线' : '离线'}</Tag>
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', fixed: 'right' as const, width: 250,
|
||||
render: (_, record: any) => (
|
||||
<Space>
|
||||
<Tooltip title="人员解绑">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbind([record.gateway_sn], 1)}>用户</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="场站解绑">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbind([record.gateway_sn], 2)}>场站</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="组织解绑">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbind([record.gateway_sn], 3)}>组织</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const selectorgId = Form.useWatch('orgId', bindForm);
|
||||
const selectsiteId = Form.useWatch('siteId', bindForm);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectorgId) {
|
||||
fetchSites(selectorgId);
|
||||
}
|
||||
}, [selectorgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectsiteId) {
|
||||
fetchUsers(selectsiteId);
|
||||
}
|
||||
}, [selectsiteId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 16, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="机场SN"
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
value={droneParams.sn}
|
||||
onChange={(e) => setDroneParams({ ...droneParams, sn: e.target.value })}
|
||||
/>
|
||||
<Select
|
||||
placeholder="状态"
|
||||
style={{ width: 120 }}
|
||||
value={droneParams.status}
|
||||
allowClear
|
||||
onChange={(val) => setDroneParams({ ...droneParams, status: val })}
|
||||
>
|
||||
<Select.Option value="1">在线</Select.Option>
|
||||
<Select.Option value="2">离线</Select.Option>
|
||||
</Select>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => setDroneParams({ pageNum: 1, pageSize: 10, sn: '', status: '' })}>重置</Button>
|
||||
<Button type="primary" icon={<LinkOutlined />} onClick={openBindModal}>绑定无人机</Button>
|
||||
<Button disabled={selectedDroneKeys.length === 0} onClick={() => handleUnbind(selectedDroneKeys, 1)}>批量解绑(用户)</Button>
|
||||
<Button disabled={selectedDroneKeys.length === 0} onClick={() => handleUnbind(selectedDroneKeys, 2)}>批量解绑(场站)</Button>
|
||||
<Button disabled={selectedDroneKeys.length === 0} onClick={() => handleUnbind(selectedDroneKeys, 3)}>批量解绑(组)</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 'calc(100vh - 220px)', overflowY: 'auto', paddingRight: '4px' }}>
|
||||
<Table
|
||||
rowSelection={{ selectedRowKeys: selectedDroneKeys, onChange: setSelectedDroneKeys as any }}
|
||||
rowKey="gateway_sn"
|
||||
columns={droneColumns}
|
||||
dataSource={droneList}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: droneParams.pageNum,
|
||||
pageSize: droneParams.pageSize,
|
||||
total: droneTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: t => `共 ${t} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setDroneParams({ ...droneParams, pageNum: page, pageSize });
|
||||
},
|
||||
}}
|
||||
scroll={{ y: 'auto', x: '700px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ display: 'inline-block', width: 4, height: 18, background: 'linear-gradient(180deg, #1890ff 0%, #096dd9 100%)', borderRadius: 2 }}></span>
|
||||
<span style={{ fontWeight: 600 }}>绑定无人机</span>
|
||||
</div>
|
||||
}
|
||||
open={bindModalVisible}
|
||||
onCancel={() => {
|
||||
setBindModalVisible(false);
|
||||
setDroneOrgOptions([]);
|
||||
setDroneSiteOptions([]);
|
||||
setDroneUserOptions([]);
|
||||
bindForm.resetFields();
|
||||
}}
|
||||
destroyOnClose={false}
|
||||
onOk={() => bindForm.submit()}
|
||||
width={540}
|
||||
>
|
||||
<Form form={bindForm} layout="vertical" onFinish={handleBind}>
|
||||
<Form.Item label={<>已选无人机({selectedDroneKeys.length}台)SN</>} name="sns" rules={[{ required: true, message: '请输入 SN' }]}>
|
||||
<Input.TextArea placeholder="例如: SN001, SN002" rows={2} disabled style={{ resize: 'none', background: '#fafafa' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>所属组织</span>
|
||||
</Space>
|
||||
}
|
||||
name="orgId" rules={[{ required: true, message: '请选择组织' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择组织"
|
||||
disabled={orgFieldDisabled}
|
||||
allowClear={isAdmin}
|
||||
onChange={(val) => {
|
||||
if (isAdmin) {
|
||||
bindForm.setFieldsValue({ siteId: undefined, userId: undefined });
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{droneOrgOptions.map((org: any) => (
|
||||
<Select.Option key={org.id || org.orgId} value={org.id || org.orgId}>{org.orgName || org.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>所属场站</span>
|
||||
</Space>
|
||||
}
|
||||
name="siteId" rules={[{ required: !siteFieldDisabled, message: '请选择场站' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择组织"
|
||||
disabled={siteFieldDisabled}
|
||||
allowClear={isAdmin || isManager}
|
||||
onChange={(val) => {
|
||||
if (isAdmin || isManager) {
|
||||
bindForm.setFieldsValue({ userId: undefined });
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{droneSiteOptions.map((site: any) => (
|
||||
<Select.Option key={site.id || site.siteId} value={site.id || site.siteId}>{site.siteName || site.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="负责人" name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
|
||||
<Select
|
||||
placeholder="请先选择场站后选择负责人"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{droneUserOptions.map((user: any) => (
|
||||
<Select.Option key={user.userId} value={user.userId}>
|
||||
{user.nickName || user.userName}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
608
src/components/SystemSetting/SmartDeviceManagement.tsx
Normal file
608
src/components/SystemSetting/SmartDeviceManagement.tsx
Normal file
@@ -0,0 +1,608 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Form,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Modal,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Typography,
|
||||
App,
|
||||
Card,
|
||||
InputNumber,
|
||||
} from 'antd';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
ReloadOutlined,
|
||||
LinkOutlined,
|
||||
DisconnectOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import {
|
||||
getAllDevice,
|
||||
bindSmartDevice,
|
||||
unbindSmartDevice,
|
||||
getDeviceAuth,
|
||||
hasPermission,
|
||||
} from '@/api/device';
|
||||
import { getOrgList } from '@/api/organization/index';
|
||||
import { getSiteByOrgId } from '@/api/stationManage/index';
|
||||
import { getUser } from '@/api/user/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/src/store';
|
||||
import utils from '@/lib/utils';
|
||||
import DeviceParamModal from '../devices/DeviceParamModal';
|
||||
|
||||
interface SmartDeviceManagementProps {
|
||||
allOrgOptions: any[];
|
||||
allSiteOptions: any[];
|
||||
allUserOptions: any[];
|
||||
onAllDataNeedRefresh?: () => void;
|
||||
}
|
||||
|
||||
export default function SmartDeviceManagement({
|
||||
allOrgOptions,
|
||||
allSiteOptions,
|
||||
allUserOptions,
|
||||
onAllDataNeedRefresh,
|
||||
}: SmartDeviceManagementProps) {
|
||||
const { modal } = App.useApp();
|
||||
const { userInfo } = useSelector((state: RootState) => state.user);
|
||||
const { stationId, currentStation } = useSelector((state: RootState) => state.station);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [smartList, setSmartList] = useState([]);
|
||||
const [selectedSmartKeys, setSelectedSmartKeys] = useState<any[]>([]);
|
||||
const [bindSmartModalVisible, setBindSmartModalVisible] = useState(false);
|
||||
const [bindSmartForm] = Form.useForm();
|
||||
const [smartParams, setSmartParams] = useState({
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
serialNumber: '',
|
||||
});
|
||||
const [smartTotal, setSmartTotal] = useState(0);
|
||||
|
||||
const [smartOrgOptions, setSmartOrgOptions] = useState([]);
|
||||
const [smartSiteOptions, setSmartSiteOptions] = useState([]);
|
||||
const [smartUserOptions, setSmartUserOptions] = useState([]);
|
||||
|
||||
const [editSmartModalVisible, setEditSmartModalVisible] = useState(false);
|
||||
const [editSmartForm] = Form.useForm();
|
||||
const [currentEditSmart, setCurrentEditSmart] = useState(null);
|
||||
|
||||
const [paramModalVisible, setParamModalVisible] = useState(false);
|
||||
const [currentParamDevice, setCurrentParamDevice] = useState(null);
|
||||
|
||||
const [paramModalVisible1, setParamModalVisible1] = useState(false);
|
||||
const [currentParamDevice1, setCurrentParamDevice1] = useState(null);
|
||||
const [paramForm1] = Form.useForm();
|
||||
|
||||
const isAdmin = userInfo?.roleKey === 'admin';
|
||||
const isManager = userInfo?.roleKey === 'manager' || userInfo?.roleKey === 'Manager';
|
||||
const orgFieldDisabled = !isAdmin;
|
||||
const siteFieldDisabled = !(isAdmin || isManager);
|
||||
|
||||
const defaultOrgId = currentStation?.orgId || userInfo?.orgId;
|
||||
const defaultSiteId = stationId || currentStation?.siteId || currentStation?.id;
|
||||
const defaultUserId = userInfo?.userId;
|
||||
|
||||
const fetchSmartList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getAllDevice(smartParams);
|
||||
if (res.code === 200) {
|
||||
setSmartList(res.rows || []);
|
||||
setSmartTotal(res.total || res.rows.length);
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('获取智能装备列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOrgs = async () => {
|
||||
try {
|
||||
const res = await getOrgList({});
|
||||
if (res.code === 200) {
|
||||
setSmartOrgOptions(res.rows || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error('获取组织列表失败', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSites = async (orgId) => {
|
||||
try {
|
||||
const res = await getSiteByOrgId({ id: orgId });
|
||||
if (res.code === 200) {
|
||||
setSmartSiteOptions(res.data || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUsers = async (siteId) => {
|
||||
try {
|
||||
const res = await getUser({
|
||||
siteId,
|
||||
pageNum: 1,
|
||||
pageSize: 1000
|
||||
});
|
||||
if (res.code === 200) {
|
||||
setSmartUserOptions(res.rows || []);
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSmartList();
|
||||
}, [smartParams]);
|
||||
|
||||
const openEditSmartModal = (record) => {
|
||||
setCurrentEditSmart(record);
|
||||
setEditSmartModalVisible(true);
|
||||
editSmartForm.setFieldsValue({
|
||||
deviceAlias: record.deviceAlias,
|
||||
serialNumber: record.serialNumber,
|
||||
onlineStatus: record.onlineStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const saveEditSmart = (values) => {
|
||||
setSmartList(prev => prev.map((item: any) => {
|
||||
if (item.serialNumber === (currentEditSmart as any)?.serialNumber) {
|
||||
return { ...item, ...values };
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
utils.message.success('装备编辑成功');
|
||||
setEditSmartModalVisible(false);
|
||||
editSmartForm.resetFields();
|
||||
setCurrentEditSmart(null);
|
||||
};
|
||||
|
||||
const openParamModal = async (record) => {
|
||||
getDevivePermisstion(record);
|
||||
};
|
||||
|
||||
const openParamModal1 = async (record) => {
|
||||
setCurrentParamDevice1(record);
|
||||
setParamModalVisible1(true);
|
||||
};
|
||||
|
||||
const getDevivePermisstion = (data) => {
|
||||
const params = {
|
||||
deviceId: data.serialNumber,
|
||||
};
|
||||
hasPermission(params).then(res => {
|
||||
if (res.code == 200) {
|
||||
if (res.data) {
|
||||
setCurrentParamDevice(data);
|
||||
setParamModalVisible(true);
|
||||
} else {
|
||||
utils.message.error('无权限');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveParam1 = async (values) => {
|
||||
try {
|
||||
const payload = {
|
||||
deviceId: (currentParamDevice1 as any)?.serialNumber,
|
||||
duration: values?.minute,
|
||||
};
|
||||
const res = await getDeviceAuth(payload);
|
||||
if (res.code === 200 && res.data == true) {
|
||||
utils.message.success('保存参数成功');
|
||||
setParamModalVisible1(false);
|
||||
} else {
|
||||
utils.message.error(res.msg || '授权失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
}
|
||||
};
|
||||
|
||||
const openBindSmartModal = async () => {
|
||||
if (!selectedSmartKeys.length) {
|
||||
return utils.message.warning('请先在列表中选中需要绑定的智能装备');
|
||||
}
|
||||
bindSmartForm.resetFields();
|
||||
setBindSmartModalVisible(true);
|
||||
setSmartOrgOptions([]);
|
||||
setSmartSiteOptions([]);
|
||||
setSmartUserOptions([]);
|
||||
const orgRes = await fetchOrgs();
|
||||
const orgList = orgRes?.code === 200 ? (orgRes?.rows || []) : [];
|
||||
const finalOrgId = defaultOrgId && orgList.some(o => (o.id || o.orgId) == defaultOrgId)
|
||||
? defaultOrgId
|
||||
: (orgList[0]?.id || orgList[0]?.orgId);
|
||||
let siteList = [];
|
||||
if (finalOrgId) {
|
||||
const siteRes = await fetchSites(finalOrgId);
|
||||
siteList = siteRes?.code === 200 ? (siteRes?.data || []) : [];
|
||||
}
|
||||
const finalSiteId = defaultSiteId && siteList.some(s => (s.id || s.siteId) == defaultSiteId)
|
||||
? defaultSiteId
|
||||
: (siteList[0]?.id || siteList[0]?.siteId);
|
||||
let userList = [];
|
||||
if (finalSiteId) {
|
||||
const userRes = await fetchUsers(finalSiteId);
|
||||
userList = userRes?.code === 200 ? (userRes.rows || []) : [];
|
||||
}
|
||||
const finalUserId = defaultUserId && userList.some(u => u.userId == defaultUserId)
|
||||
? defaultUserId
|
||||
: (userList[0]?.userId);
|
||||
setTimeout(() => {
|
||||
bindSmartForm.setFieldsValue({
|
||||
deviceIds: selectedSmartKeys.join(', '),
|
||||
orgId: finalOrgId,
|
||||
siteId: finalSiteId,
|
||||
userId: Number(finalUserId) || '',
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleBindSmart = async (values) => {
|
||||
try {
|
||||
const payload = {
|
||||
siteId: values.siteId,
|
||||
orgId: values.orgId,
|
||||
userId: values.userId,
|
||||
deviceIds: values.deviceIds.split(',').map(s => s.trim()),
|
||||
};
|
||||
const res = await bindSmartDevice(payload);
|
||||
if (res.code === 200 && res.data == true) {
|
||||
utils.message.success('绑定成功');
|
||||
setBindSmartModalVisible(false);
|
||||
bindSmartForm.resetFields();
|
||||
fetchSmartList();
|
||||
onAllDataNeedRefresh?.();
|
||||
} else {
|
||||
utils.message.error('绑定失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnbindSmart = (deviceIds, type) => {
|
||||
const validIds = (deviceIds || []).filter(id => id);
|
||||
if (validIds.length === 0) {
|
||||
return utils.message.warning('未选择有效设备,无法解绑');
|
||||
}
|
||||
const typeText = { 1: '人员', 2: '场站', 3: '组织' }[type as number];
|
||||
modal.confirm({
|
||||
title: '确认解绑',
|
||||
content: `确定要为选中的智能装备清除${typeText}绑定吗?`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await unbindSmartDevice({ deviceIds: validIds, type });
|
||||
if (res.code === 200) {
|
||||
utils.message.success('解绑成功');
|
||||
fetchSmartList();
|
||||
setSelectedSmartKeys([]);
|
||||
onAllDataNeedRefresh?.();
|
||||
} else {
|
||||
utils.message.error(res.msg || '解绑失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const smartColumns = [
|
||||
{ title: '设备名称', width: 150, dataIndex: 'deviceAlias', key: 'name' },
|
||||
{ title: '设备码', width: 280, dataIndex: 'serialNumber', key: 'sn' },
|
||||
{
|
||||
title: '所属组织', width: 150, key: 'orgName',
|
||||
render: (_, record: any) => {
|
||||
const org = allOrgOptions.find(o => (o.orgId || o.id) == record.orgId);
|
||||
return org ? (org.orgName || org.name) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '所属场站', width: 150, key: 'siteName',
|
||||
render: (_, record: any) => {
|
||||
const site = allSiteOptions.find(s => (s.siteId || s.id) == record.siteId);
|
||||
return site ? (site.siteName || site.name) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '负责人', width: 150, key: 'userName',
|
||||
render: (_, record: any) => {
|
||||
const user = allUserOptions.find(u => u.userId == record.userId);
|
||||
return user ? (user.nickName || user.userName) : <Text type="secondary">未绑定</Text>;
|
||||
},
|
||||
},
|
||||
{ title: '在线状态', width: 120, dataIndex: 'onlineStatus', key: 'online', render: s => <Tag color={s === 'ONLINE' || s === 1 ? 'green' : 'red'}>{s === 'ONLINE' || s === 1 ? '在线' : '离线'}</Tag> },
|
||||
{
|
||||
title: '操作', width: 280, key: 'action', fixed: 'right' as const,
|
||||
render: (_, record: any) => (
|
||||
<Space>
|
||||
<Tooltip title="清除人员绑定">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbindSmart([record.serialNumber], 1)}>用户</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="清除场站绑定">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbindSmart([record.serialNumber], 2)}>场站</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="清除组织绑定">
|
||||
<Button size="small" icon={<DisconnectOutlined />} onClick={() => handleUnbindSmart([record.serialNumber], 3)}>组织</Button>
|
||||
</Tooltip>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => openParamModal(record)}>参数</Button>
|
||||
{userInfo?.userId == 1 && <Button size="small" icon={<EditOutlined />} onClick={() => openParamModal1(record)}>授权</Button>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const selectorgId = Form.useWatch('orgId', bindSmartForm);
|
||||
const selectsiteId = Form.useWatch('siteId', bindSmartForm);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectorgId) {
|
||||
fetchSites(selectorgId);
|
||||
}
|
||||
}, [selectorgId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectsiteId) {
|
||||
fetchUsers(selectsiteId);
|
||||
}
|
||||
}, [selectsiteId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="设备码"
|
||||
style={{ width: 240 }}
|
||||
value={smartParams.serialNumber}
|
||||
allowClear
|
||||
onChange={(e) => setSmartParams({ ...smartParams, serialNumber: e.target.value.trim() })}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => setSmartParams({ pageNum: 1, pageSize: 10, serialNumber: '' })}>重置</Button>
|
||||
<Button type="primary" icon={<LinkOutlined />} onClick={openBindSmartModal}>绑定装备</Button>
|
||||
<Button disabled={selectedSmartKeys.length === 0} onClick={() => handleUnbindSmart(selectedSmartKeys, 1)}>批量解绑(用户)</Button>
|
||||
<Button disabled={selectedSmartKeys.length === 0} onClick={() => handleUnbindSmart(selectedSmartKeys, 2)}>批量解绑(场站)</Button>
|
||||
<Button disabled={selectedSmartKeys.length === 0} onClick={() => handleUnbindSmart(selectedSmartKeys, 3)}>批量解绑(组织)</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 'calc(100vh - 220px)', overflowY: 'auto', paddingRight: '4px' }}>
|
||||
<Table
|
||||
rowSelection={{ selectedRowKeys: selectedSmartKeys, onChange: setSelectedSmartKeys as any }}
|
||||
rowKey={(record: any) => record.serialNumber}
|
||||
columns={smartColumns}
|
||||
dataSource={smartList}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: smartParams.pageNum,
|
||||
pageSize: smartParams.pageSize,
|
||||
total: smartTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (page, pageSize) => {
|
||||
setSmartParams({ pageNum: page, pageSize });
|
||||
},
|
||||
}}
|
||||
scroll={{ y: 'auto', x: '700px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ display: 'inline-block', width: 4, height: 18, background: 'linear-gradient(180deg, #52c41a 0%, #389e0d 100%)', borderRadius: 2 }}></span>
|
||||
<span style={{ fontWeight: 600 }}>绑定智能装备</span>
|
||||
</div>
|
||||
}
|
||||
open={bindSmartModalVisible}
|
||||
onCancel={() => {
|
||||
setBindSmartModalVisible(false);
|
||||
setSmartOrgOptions([]);
|
||||
setSmartSiteOptions([]);
|
||||
setSmartUserOptions([]);
|
||||
bindSmartForm.resetFields();
|
||||
}}
|
||||
destroyOnClose={false}
|
||||
onOk={() => bindSmartForm.submit()}
|
||||
width={540}
|
||||
>
|
||||
<Form form={bindSmartForm} layout="vertical" onFinish={handleBindSmart}>
|
||||
<Form.Item label={<>已选智能装备({selectedSmartKeys.length}台)ID</>} name="deviceIds" rules={[{ required: true, message: '请选择装备' }]}>
|
||||
<Input.TextArea placeholder="例如: ID001, ID002" rows={2} disabled style={{ resize: 'none', background: '#fafafa' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>所属组织</span>
|
||||
</Space>
|
||||
}
|
||||
name="orgId" rules={[{ required: true, message: '请选择组织' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择组织"
|
||||
disabled={orgFieldDisabled}
|
||||
allowClear={isAdmin}
|
||||
onChange={(val) => {
|
||||
if (isAdmin) {
|
||||
bindSmartForm.setFieldsValue({ siteId: undefined, userId: undefined });
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{smartOrgOptions.map((org: any) => (
|
||||
<Select.Option key={org.id || org.orgId} value={org.id || org.orgId}>{org.orgName || org.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space size={6}>
|
||||
<span>所属场站</span>
|
||||
</Space>
|
||||
}
|
||||
name="siteId" rules={[{ required: !siteFieldDisabled, message: '请选择场站' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请先选择组织"
|
||||
disabled={siteFieldDisabled}
|
||||
allowClear={isAdmin || isManager}
|
||||
onChange={(val) => {
|
||||
if (isAdmin || isManager) {
|
||||
bindSmartForm.setFieldsValue({ userId: undefined });
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{smartSiteOptions.map((site: any) => (
|
||||
<Select.Option key={site.id || site.siteId} value={site.id || site.siteId}>{site.siteName || site.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="负责人" name="userId" rules={[{ required: true, message: '请选择负责人' }]}>
|
||||
<Select
|
||||
placeholder="请先选择场站后选择负责人"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{smartUserOptions.map((user: any) => (
|
||||
<Select.Option key={user.userId} value={user.userId}>
|
||||
{user.nickName || user.userName}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="编辑智能装备信息"
|
||||
open={editSmartModalVisible}
|
||||
onCancel={() => {
|
||||
setEditSmartModalVisible(false);
|
||||
setCurrentEditSmart(null);
|
||||
editSmartForm.resetFields();
|
||||
}}
|
||||
onOk={() => editSmartForm.submit()}
|
||||
width={480}
|
||||
>
|
||||
<Form form={editSmartForm} layout="vertical" onFinish={saveEditSmart}>
|
||||
<Form.Item label="设备名称" name="deviceAlias" rules={[{ required: true, message: '请填写设备名称' }]}>
|
||||
<Input placeholder="请输入设备名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="设备序列号" name="serialNumber" rules={[{ required: true, message: '请填写设备码' }]}>
|
||||
<Input placeholder="请输入设备序列号" />
|
||||
</Form.Item>
|
||||
<Form.Item label="在线状态" name="onlineStatus" rules={[{ required: true, message: '请选择在线状态' }]}>
|
||||
<Select placeholder="选择在线状态">
|
||||
<Select.Option value="ONLINE">在线</Select.Option>
|
||||
<Select.Option value="OFFLINE">离线</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 18,
|
||||
background: 'linear-gradient(180deg, #1890ff 0%, #096dd9 100%)',
|
||||
borderRadius: 2,
|
||||
}}></span>
|
||||
<span style={{ fontWeight: 600 }}>设备授权</span>
|
||||
<span style={{ color: '#8c8c8c', fontSize: 13, fontWeight: 'normal' }}>
|
||||
- {(currentParamDevice1 as any)?.deviceAlias || (currentParamDevice1 as any)?.serialNumber}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
open={paramModalVisible1}
|
||||
onCancel={() => {
|
||||
setParamModalVisible1(false);
|
||||
setCurrentParamDevice1(null);
|
||||
}}
|
||||
footer={null}
|
||||
width={560}
|
||||
>
|
||||
<Form
|
||||
form={paramForm1}
|
||||
layout="vertical"
|
||||
onFinish={saveParam1}
|
||||
>
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
background: 'linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)',
|
||||
border: '1px solid #91caeb',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#095b91', fontSize: 13 }}>
|
||||
<span>💡</span>
|
||||
<span>设置设备操作授权,授权到期后设备将失去对应操作权限</span>
|
||||
</div>
|
||||
</Card>
|
||||
<Form.Item
|
||||
label={<span style={{ fontWeight: 500 }}>授权时间(min)</span>}
|
||||
name="minute"
|
||||
rules={[
|
||||
{ required: true, message: '请输入授权剩余时间' },
|
||||
{ type: 'number', min: 0, message: '授权时间不能小于0' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={0}
|
||||
placeholder="请输入授权剩余时间"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginTop: 20, marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<Button onClick={() => setParamModalVisible1(false)} style={{ borderRadius: 8, padding: '4px 20px' }}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" style={{ borderRadius: 8, padding: '4px 24px' }}>保存授权</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<DeviceParamModal
|
||||
open={paramModalVisible}
|
||||
device={currentParamDevice as any}
|
||||
onCancel={() => {
|
||||
setParamModalVisible(false);
|
||||
setCurrentParamDevice(null);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,22 +2,20 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
InputNumber,
|
||||
Input,
|
||||
Checkbox,
|
||||
Slider,
|
||||
Button,
|
||||
Space,
|
||||
Spin,
|
||||
} from 'antd';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store';
|
||||
import { deviceRunParamSelectByDeviceId, deviceRunParamSave } from '@/api/stationManage/index';
|
||||
import utils from '@/lib/utils';
|
||||
|
||||
const GainSliderInput = ({ value = 0.5, onChange, disabled, min = 0.1, max = 1, step = 0.01 }) => {
|
||||
const GainSliderInput = ({ value, onChange, disabled, min = 0.1, max = 1, step = 0.01 }) => {
|
||||
const handleSliderChange = (val) => {
|
||||
onChange?.(val);
|
||||
};
|
||||
@@ -165,11 +163,13 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
|
||||
setParamData(res.data);
|
||||
paramForm.setFieldsValue({ ...res.data });
|
||||
} else {
|
||||
utils.message.error('获取参数失败');
|
||||
|
||||
setParamData(null);
|
||||
paramForm.resetFields();
|
||||
}
|
||||
} else {
|
||||
utils.message.error(res.msg || '获取参数失败');
|
||||
utils.message.error('获取参数失败');
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('获取参数失败');
|
||||
@@ -179,6 +179,7 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
|
||||
};
|
||||
|
||||
const saveParam = async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const payload = {
|
||||
leftForwardGain: Math.max(0.1, Math.min(1, values.leftForwardGain >= 10 ? values.leftForwardGain / 10 : values.leftForwardGain)),
|
||||
@@ -200,6 +201,8 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
|
||||
}
|
||||
} catch (err) {
|
||||
utils.message.error('操作异常');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,85 +234,80 @@ const DeviceParamModal: React.FC<DeviceParamModalProps> = ({ open, device, onCan
|
||||
onOk={() => paramForm.submit()}
|
||||
confirmLoading={loading}
|
||||
width={780}
|
||||
bodyStyle={{ padding: '16px 24px 0px' }}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto', paddingRight: 8 }}>
|
||||
<Form form={paramForm} layout="vertical" onFinish={saveParam}>
|
||||
{/* 加载时表单区域简单遮罩提示(打开弹窗拉取参数时的loading反馈) */}
|
||||
<div style={{ maxHeight: '60vh', overflowY: 'auto', paddingRight: 8, position: 'relative' }}>
|
||||
<Spin spinning={loading} tip="加载中...">
|
||||
<Form form={paramForm} layout="vertical" onFinish={saveParam}>
|
||||
|
||||
<Row gutter={16}>
|
||||
{editableKeys.map((key) => {
|
||||
const field = paramFieldConfigs.find(f => f.key === key);
|
||||
if (!field) return null;
|
||||
const isGainSliderField = ['leftForwardGain', 'leftBackwardGain', 'rightForwardGain', 'rightBackwardGain'].includes(key);
|
||||
const disabled = !field.editable;
|
||||
return (
|
||||
<Col span={12} key={key}>
|
||||
<Form.Item
|
||||
label={<span style={{ fontWeight: 500 }}>{field.label}</span>}
|
||||
name={field.key}
|
||||
>
|
||||
{isGainSliderField ? (
|
||||
<GainSliderInput
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={0.01}
|
||||
/>
|
||||
) : (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
placeholder={typeof field.min === 'number' ? `${field.min}~${field.max}` : ''}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
{editableKeys.map((key) => {
|
||||
const field = paramFieldConfigs.find(f => f.key === key);
|
||||
if (!field) return null;
|
||||
const isGainSliderField = ['leftForwardGain', 'leftBackwardGain', 'rightForwardGain', 'rightBackwardGain'].includes(key);
|
||||
const disabled = !field.editable;
|
||||
return (
|
||||
<Col span={12} key={key}>
|
||||
<Form.Item
|
||||
label={<span style={{ fontWeight: 500 }}>{field.label}</span>}
|
||||
name={field.key}
|
||||
>
|
||||
{isGainSliderField ? (
|
||||
<GainSliderInput
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={0.01}
|
||||
/>
|
||||
) : (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
placeholder={typeof field.min === 'number' ? `${field.min}~${field.max}` : ''}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
|
||||
<Row gutter={16}>
|
||||
{paramFieldConfigs.filter(f => !editableKeys.includes(f.key)).map((field) => {
|
||||
const disabled = !field.editable;
|
||||
return (
|
||||
<Col span={12} key={field.key}>
|
||||
<Form.Item
|
||||
label={field.label}
|
||||
name={field.key}
|
||||
valuePropName={field.type === 'Checkbox' ? 'checked' : undefined}
|
||||
>
|
||||
{field.type === 'InputNumber' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
placeholder={typeof field.min === 'number' ? `${field.min}~${field.max}` : ''}
|
||||
/>
|
||||
) : field.type === 'Checkbox' ? (
|
||||
<Checkbox disabled={disabled} />
|
||||
) : (
|
||||
<Input disabled={disabled} placeholder="无数据" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{ marginTop: 20, marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<Button onClick={handleCancel} style={{ borderRadius: 8, padding: '4px 24px' }}>取消</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading} style={{ borderRadius: 8, padding: '4px 28px' }}>保存</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Row gutter={16}>
|
||||
{paramFieldConfigs.filter(f => !editableKeys.includes(f.key)).map((field) => {
|
||||
const disabled = !field.editable;
|
||||
return (
|
||||
<Col span={12} key={field.key}>
|
||||
<Form.Item
|
||||
label={field.label}
|
||||
name={field.key}
|
||||
valuePropName={field.type === 'Checkbox' ? 'checked' : undefined}
|
||||
>
|
||||
{field.type === 'InputNumber' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
disabled={disabled}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
step={field.step}
|
||||
placeholder={typeof field.min === 'number' ? `${field.min}~${field.max}` : ''}
|
||||
/>
|
||||
) : field.type === 'Checkbox' ? (
|
||||
<Checkbox disabled={disabled} />
|
||||
) : (
|
||||
<Input disabled={disabled} placeholder="无数据" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Form>
|
||||
</Spin>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user