Files
web-Iot/src/components/SystemSetting/DroneManagement.tsx

457 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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