457 lines
14 KiB
TypeScript
457 lines
14 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|