2026-04-08 09:29:06 +00:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2026-01-21 07:21:17 +00:00
|
|
|
|
import {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Space,
|
2026-03-26 09:32:31 +00:00
|
|
|
|
Drawer,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
Form,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
App,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Switch,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
PlusOutlined,
|
2026-03-26 09:32:31 +00:00
|
|
|
|
SaveOutlined,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
SyncOutlined,
|
|
|
|
|
|
MonitorOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
LinkOutlined,
|
|
|
|
|
|
WifiOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
SafetyCertificateOutlined,
|
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-21 07:21:17 +00:00
|
|
|
|
import apiClient from '../../utils/apiClient';
|
|
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
2026-04-03 16:25:53 +00:00
|
|
|
|
import ActionButton from '../../components/ActionButton';
|
2026-04-08 09:29:06 +00:00
|
|
|
|
import useSystemPageSize from '../../hooks/useSystemPageSize';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
|
|
|
|
|
|
const ONLINE_MINUTES = 10;
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
|
|
|
|
|
const TerminalManagement = () => {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const { message, modal } = App.useApp();
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const pageSize = useSystemPageSize(10);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const [terminals, setTerminals] = useState([]);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
const [terminalTypes, setTerminalTypes] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
2026-01-21 07:21:17 +00:00
|
|
|
|
const [keyword, setKeyword] = useState('');
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const [filterType, setFilterType] = useState('all');
|
|
|
|
|
|
const [filterStatus, setFilterStatus] = useState('all');
|
|
|
|
|
|
const [filterActivation, setFilterActivation] = useState('all');
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
const [showDrawer, setShowDrawer] = useState(false);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
|
|
const [selectedTerminal, setSelectedTerminal] = useState(null);
|
|
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchTerminalTypes = useCallback(async () => {
|
2026-01-21 07:21:17 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
|
|
|
|
|
|
params: { parent_code: 'TERMINAL' },
|
|
|
|
|
|
});
|
2026-01-21 07:21:17 +00:00
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
setTerminalTypes(response.data.items || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch terminal types:', error);
|
|
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, []);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchTerminals = useCallback(async () => {
|
2026-01-21 07:21:17 +00:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
|
|
|
|
|
|
params: { page: 1, size: 10000 },
|
|
|
|
|
|
});
|
2026-01-21 07:21:17 +00:00
|
|
|
|
if (response.code === '200') {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setTerminals(response.data.items || []);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('获取终端列表失败');
|
2026-01-21 07:21:17 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, [message]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchTerminalTypes();
|
|
|
|
|
|
fetchTerminals();
|
|
|
|
|
|
}, [fetchTerminalTypes, fetchTerminals]);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleOpenModal = (terminal = null) => {
|
|
|
|
|
|
if (terminal) {
|
|
|
|
|
|
setIsEditing(true);
|
|
|
|
|
|
setSelectedTerminal(terminal);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
...terminal,
|
|
|
|
|
|
status: terminal.status === 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
setSelectedTerminal(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
terminal_type: terminalTypes[0]?.dict_code,
|
|
|
|
|
|
status: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
setShowDrawer(true);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleSave = async () => {
|
2026-01-21 07:21:17 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
const payload = {
|
|
|
|
|
|
...values,
|
|
|
|
|
|
status: values.status ? 1 : 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-21 07:21:17 +00:00
|
|
|
|
if (isEditing) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.TERMINALS.UPDATE(selectedTerminal.id)), payload);
|
|
|
|
|
|
message.success('终端更新成功');
|
2026-01-21 07:21:17 +00:00
|
|
|
|
} else {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload);
|
|
|
|
|
|
message.success('终端创建成功');
|
2026-01-21 07:21:17 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
setShowDrawer(false);
|
2026-01-21 07:21:17 +00:00
|
|
|
|
fetchTerminals();
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (!error.errorFields) {
|
|
|
|
|
|
message.error('保存失败');
|
|
|
|
|
|
}
|
2026-01-21 07:21:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleDelete = (item) => {
|
|
|
|
|
|
modal.confirm({
|
|
|
|
|
|
title: '删除终端',
|
|
|
|
|
|
content: `确定要删除 IMEI 为 ${item.imei} 的终端吗?`,
|
|
|
|
|
|
okText: '确定',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await apiClient.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id)));
|
|
|
|
|
|
message.success('删除成功');
|
|
|
|
|
|
fetchTerminals();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-21 07:21:17 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleToggleStatus = async (item, checked) => {
|
2026-01-21 07:21:17 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const newStatus = checked ? 1 : 0;
|
|
|
|
|
|
await apiClient.post(buildApiUrl(API_ENDPOINTS.TERMINALS.STATUS(item.id)), null, {
|
|
|
|
|
|
params: { status: newStatus },
|
|
|
|
|
|
});
|
|
|
|
|
|
setTerminals((prev) => prev.map((terminal) => (
|
|
|
|
|
|
terminal.id === item.id ? { ...terminal, status: newStatus } : terminal
|
|
|
|
|
|
)));
|
|
|
|
|
|
message.success(`已${newStatus === 1 ? '启用' : '停用'}终端`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('状态更新失败');
|
2026-01-21 07:21:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const getTerminalTypeLabel = (code) => (
|
|
|
|
|
|
terminalTypes.find((terminalType) => terminalType.dict_code === code)?.label_cn || code
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const isOnline = (lastOnlineAt) => {
|
|
|
|
|
|
if (!lastOnlineAt) return false;
|
|
|
|
|
|
const diffMs = Date.now() - new Date(lastOnlineAt).getTime();
|
|
|
|
|
|
return diffMs >= 0 && diffMs <= ONLINE_MINUTES * 60 * 1000;
|
2026-01-21 07:21:17 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const filteredTerminals = useMemo(() => terminals.filter((terminal) => {
|
|
|
|
|
|
if (filterType !== 'all' && terminal.terminal_type !== filterType) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filterStatus !== 'all' && terminal.status !== Number.parseInt(filterStatus, 10)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filterActivation === 'activated' && terminal.is_activated !== 1) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filterActivation === 'pending' && terminal.is_activated === 1) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!keyword) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const query = keyword.toLowerCase();
|
|
|
|
|
|
return [
|
|
|
|
|
|
terminal.imei,
|
|
|
|
|
|
terminal.terminal_name,
|
|
|
|
|
|
terminal.current_username,
|
|
|
|
|
|
terminal.current_user_caption,
|
|
|
|
|
|
terminal.mac_address,
|
|
|
|
|
|
terminal.ip_address,
|
|
|
|
|
|
].some((field) => String(field || '').toLowerCase().includes(query));
|
|
|
|
|
|
}), [terminals, filterType, filterStatus, filterActivation, keyword]);
|
|
|
|
|
|
|
|
|
|
|
|
const activeCount = useMemo(() => terminals.filter((terminal) => terminal.status === 1).length, [terminals]);
|
|
|
|
|
|
const activatedCount = useMemo(() => terminals.filter((terminal) => terminal.is_activated === 1).length, [terminals]);
|
|
|
|
|
|
const onlineCount = useMemo(() => terminals.filter((terminal) => isOnline(terminal.last_online_at)).length, [terminals]);
|
|
|
|
|
|
|
2026-02-12 07:34:12 +00:00
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'IMEI',
|
|
|
|
|
|
dataIndex: 'imei',
|
|
|
|
|
|
key: 'imei',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 160,
|
|
|
|
|
|
render: (text) => <Text code>{text}</Text>,
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '终端信息',
|
|
|
|
|
|
key: 'terminal',
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text strong>{record.terminal_name || '未命名终端'}</Text>
|
|
|
|
|
|
<Space size={8}>
|
|
|
|
|
|
<Tag color="orange">{getTerminalTypeLabel(record.terminal_type)}</Tag>
|
|
|
|
|
|
{isOnline(record.last_online_at) ? <Tag color="green" icon={<WifiOutlined />}>在线</Tag> : <Tag>离线</Tag>}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '绑定账号',
|
|
|
|
|
|
key: 'user',
|
|
|
|
|
|
width: 170,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
record.current_user_caption ? (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text>{record.current_user_caption}</Text>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 11 }}>({record.current_username})</Text>
|
|
|
|
|
|
</Space>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
) : (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Text type="secondary">未绑定</Text>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
)
|
2026-03-26 06:55:12 +00:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '网络与固件',
|
|
|
|
|
|
key: 'network',
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text type="secondary">IP:{record.ip_address || '-'}</Text>
|
|
|
|
|
|
<Text type="secondary">MAC:{record.mac_address || '-'}</Text>
|
|
|
|
|
|
<Text type="secondary">固件:{record.firmware_version || '-'}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
key: 'status',
|
2026-03-26 09:32:31 +00:00
|
|
|
|
width: 110,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (_, record) => (
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Switch
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
checked={record.status === 1}
|
|
|
|
|
|
onChange={(checked) => handleToggleStatus(record, checked)}
|
|
|
|
|
|
/>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '激活情况',
|
|
|
|
|
|
dataIndex: 'is_activated',
|
2026-02-12 07:34:12 +00:00
|
|
|
|
key: 'is_activated',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (value) => (
|
|
|
|
|
|
value === 1
|
|
|
|
|
|
? <Badge status="success" text="已激活" />
|
|
|
|
|
|
: <Badge status="default" text="未激活" />
|
|
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '最后在线',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
dataIndex: 'last_online_at',
|
2026-02-12 07:34:12 +00:00
|
|
|
|
key: 'last_online_at',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (time) => (time ? new Date(time).toLocaleString() : '-'),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
fixed: 'right',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (_, record) => (
|
2026-04-03 16:25:53 +00:00
|
|
|
|
<Space size={6}>
|
|
|
|
|
|
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
|
|
|
|
|
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-02-12 07:34:12 +00:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-01-21 07:21:17 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="terminal-management">
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<AdminModuleShell
|
|
|
|
|
|
icon={<MonitorOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />}
|
|
|
|
|
|
title="终端管理"
|
|
|
|
|
|
subtitle="维护终端设备全生命周期:注册、绑定、激活、启停与在线状态监控。"
|
|
|
|
|
|
rightActions={(
|
|
|
|
|
|
<Space>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button icon={<ReloadOutlined />} className="btn-soft-blue" onClick={fetchTerminals} loading={loading}>刷新</Button>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>添加终端</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
stats={[
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '设备总数',
|
|
|
|
|
|
value: terminals.length,
|
|
|
|
|
|
icon: <MonitorOutlined />,
|
|
|
|
|
|
tone: 'blue',
|
|
|
|
|
|
desc: '已纳入系统管理的终端设备总量',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '启用设备',
|
|
|
|
|
|
value: activeCount,
|
|
|
|
|
|
icon: <CheckCircleOutlined />,
|
|
|
|
|
|
tone: 'green',
|
|
|
|
|
|
desc: '允许接入平台并正常提供服务的设备',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '已激活设备',
|
|
|
|
|
|
value: activatedCount,
|
|
|
|
|
|
icon: <SafetyCertificateOutlined />,
|
|
|
|
|
|
tone: 'violet',
|
|
|
|
|
|
desc: '已完成激活或完成初始化绑定的设备',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '在线设备',
|
|
|
|
|
|
value: onlineCount,
|
|
|
|
|
|
icon: <WifiOutlined />,
|
|
|
|
|
|
tone: 'cyan',
|
|
|
|
|
|
desc: `最近 ${ONLINE_MINUTES} 分钟内有心跳上报的设备`,
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
toolbar={(
|
|
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索 IMEI、名称、账号或网络信息..."
|
|
|
|
|
|
prefix={<SearchOutlined />}
|
2026-01-21 07:21:17 +00:00
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChange={(e) => setKeyword(e.target.value)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
style={{ width: 280 }}
|
|
|
|
|
|
allowClear
|
2026-01-21 07:21:17 +00:00
|
|
|
|
/>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Select value={filterType} onChange={setFilterType} style={{ width: 160 }}>
|
|
|
|
|
|
<Select.Option value="all">所有类型</Select.Option>
|
|
|
|
|
|
{terminalTypes.map((terminalType) => (
|
|
|
|
|
|
<Select.Option key={terminalType.dict_code} value={terminalType.dict_code}>
|
|
|
|
|
|
{terminalType.label_cn}
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select value={filterStatus} onChange={setFilterStatus} style={{ width: 120 }}>
|
|
|
|
|
|
<Select.Option value="all">所有状态</Select.Option>
|
|
|
|
|
|
<Select.Option value="1">启用</Select.Option>
|
|
|
|
|
|
<Select.Option value="0">停用</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select value={filterActivation} onChange={setFilterActivation} style={{ width: 130 }}>
|
|
|
|
|
|
<Select.Option value="all">全部激活状态</Select.Option>
|
|
|
|
|
|
<Select.Option value="activated">已激活</Select.Option>
|
|
|
|
|
|
<Select.Option value="pending">未激活</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Tooltip title={`最近 ${ONLINE_MINUTES} 分钟内有心跳即视为在线`}>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button icon={<SyncOutlined />} className="btn-soft-blue" onClick={fetchTerminals} loading={loading} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="console-table">
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={filteredTerminals}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
scroll={{ x: 1220 }}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
pagination={{ pageSize, showTotal: (count) => `共 ${count} 条记录` }}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
/>
|
2026-01-21 07:21:17 +00:00
|
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</AdminModuleShell>
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Drawer
|
|
|
|
|
|
open={showDrawer}
|
2026-01-21 07:21:17 +00:00
|
|
|
|
title={isEditing ? '编辑终端' : '添加终端'}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
placement="right"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width={720}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
onClose={() => setShowDrawer(false)}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
extra={(
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
|
|
|
|
|
|
{isEditing ? '保存修改' : '创建终端'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
2026-01-21 07:21:17 +00:00
|
|
|
|
>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Form form={form} layout="vertical" style={{ marginTop: 20 }}>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="imei" label="IMEI 号" rules={[{ required: true, message: '请输入 IMEI 号' }]}>
|
|
|
|
|
|
<Input placeholder="设备唯一识别码" disabled={isEditing} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="terminal_name" label="终端名称">
|
|
|
|
|
|
<Input placeholder="例如:会议室终端 A" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="terminal_type" label="终端类型" rules={[{ required: true, message: '请选择终端类型' }]}>
|
|
|
|
|
|
<Select placeholder="选择终端硬件类型">
|
|
|
|
|
|
{terminalTypes.map((terminalType) => (
|
|
|
|
|
|
<Select.Option key={terminalType.dict_code} value={terminalType.dict_code}>
|
|
|
|
|
|
{terminalType.label_cn}
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="firmware_version" label="固件版本">
|
|
|
|
|
|
<Input placeholder="例如:v1.0.3" prefix={<LinkOutlined />} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="mac_address" label="MAC 地址">
|
|
|
|
|
|
<Input placeholder="例如:00:11:22:33:44:55" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="status" label="启用状态" valuePropName="checked">
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Switch />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2026-01-21 07:21:17 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Form.Item name="description" label="备注说明">
|
|
|
|
|
|
<TextArea rows={3} placeholder="记录设备部署位置、资产编号或特殊说明" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
</Drawer>
|
2026-01-21 07:21:17 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default TerminalManagement;
|