优化设备管理界面讲将5个子页面抽离出组件 轻量化 也优化了 两个组件内弹窗的数据影响 优化了 设备参数弹窗的 loading效果

This commit is contained in:
mmc
2026-08-07 09:56:12 +08:00
parent 0c8224e424
commit 1fc5c2e0b4
6 changed files with 1778 additions and 1129 deletions

View 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} 条`,
}}
/>
</>
);
}

View 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

View 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>
</>
);
}

View 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={() => {
}}
/>
</>
);
}

View File

@@ -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>
);