imetting/frontend/src/pages/admin/TerminalManagement.jsx

472 lines
15 KiB
React
Raw Normal View History

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-04-08 11:19:33 +00:00
import httpService from '../../services/httpService';
2026-01-21 07:21:17 +00:00
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-04-08 11:19:33 +00:00
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')), {
2026-03-26 06:55:12 +00:00
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-04-08 11:19:33 +00:00
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.TERMINALS.LIST), {
2026-03-26 06:55:12 +00:00
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-04-08 11:19:33 +00:00
await httpService.put(buildApiUrl(API_ENDPOINTS.TERMINALS.UPDATE(selectedTerminal.id)), payload);
2026-03-26 06:55:12 +00:00
message.success('终端更新成功');
2026-01-21 07:21:17 +00:00
} else {
2026-04-08 11:19:33 +00:00
await httpService.post(buildApiUrl(API_ENDPOINTS.TERMINALS.CREATE), payload);
2026-03-26 06:55:12 +00:00
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 {
2026-04-08 11:19:33 +00:00
await httpService.delete(buildApiUrl(API_ENDPOINTS.TERMINALS.DELETE(item.id)));
2026-03-26 06:55:12 +00:00
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;
2026-04-08 11:19:33 +00:00
await httpService.post(buildApiUrl(API_ENDPOINTS.TERMINALS.STATUS(item.id)), null, {
2026-03-26 06:55:12 +00:00
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]);
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-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-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-03-26 06:55:12 +00:00
<Text type="secondary">未绑定</Text>
)
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>
),
},
{
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-03-26 06:55:12 +00:00
title: '激活情况',
dataIndex: 'is_activated',
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="未激活" />
),
},
{
title: '最后在线',
2026-03-26 06:55:12 +00:00
dataIndex: 'last_online_at',
key: 'last_online_at',
2026-03-26 06:55:12 +00:00
width: 180,
render: (time) => (time ? new Date(time).toLocaleString() : '-'),
},
{
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-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;