287 lines
8.3 KiB
TypeScript
287 lines
8.3 KiB
TypeScript
import { useState, useCallback, useMemo } from 'react';
|
||
import {
|
||
Button, Form, Input, message, Modal, Popconfirm, Select, Space, Table, Tag,
|
||
} from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import dayjs from 'dayjs';
|
||
|
||
import {
|
||
bleGatewayApi,
|
||
type BleGateway,
|
||
type CreateBleGatewayReq,
|
||
type UpdateBleGatewayReq,
|
||
GATEWAY_STATUS_OPTIONS,
|
||
GATEWAY_STATUS_COLOR,
|
||
GATEWAY_STATUS_LABEL,
|
||
} from '../../api/health/bleGateways';
|
||
import { PageContainer } from '../../components/PageContainer';
|
||
import { usePermission } from '../../hooks/usePermission';
|
||
import { useNavigate } from 'react-router-dom';
|
||
|
||
export default function BleGatewayList() {
|
||
const { hasPermission } = usePermission('health.ble-gateways.manage');
|
||
const navigate = useNavigate();
|
||
|
||
const [data, setData] = useState<BleGateway[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [page, setPage] = useState(1);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const [statusFilter, setStatusFilter] = useState<string | undefined>();
|
||
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [editRecord, setEditRecord] = useState<BleGateway | null>(null);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [form] = Form.useForm();
|
||
|
||
const pageSize = 20;
|
||
|
||
const fetchData = useCallback(async (p: number, status?: string) => {
|
||
setLoading(true);
|
||
try {
|
||
const resp = await bleGatewayApi.list({ page: p, page_size: pageSize, status });
|
||
setData(resp.data);
|
||
setTotal(resp.total);
|
||
setPage(p);
|
||
} catch {
|
||
message.error('加载网关列表失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const handleSearch = () => {
|
||
fetchData(1, statusFilter);
|
||
};
|
||
|
||
const handleCreate = () => {
|
||
setEditRecord(null);
|
||
form.resetFields();
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleEdit = (record: BleGateway) => {
|
||
setEditRecord(record);
|
||
form.setFieldsValue({
|
||
name: record.name,
|
||
status: record.status,
|
||
firmware_version: record.firmware_version,
|
||
});
|
||
setModalOpen(true);
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setSubmitting(true);
|
||
|
||
if (editRecord) {
|
||
const req: UpdateBleGatewayReq & { version: number } = {
|
||
...values,
|
||
version: editRecord.version,
|
||
};
|
||
await bleGatewayApi.update(editRecord.gateway_id, req);
|
||
message.success('网关已更新');
|
||
} else {
|
||
const req: CreateBleGatewayReq = values;
|
||
const created = await bleGatewayApi.create(req);
|
||
Modal.success({
|
||
title: '网关创建成功',
|
||
content: (
|
||
<div>
|
||
<p><strong>网关 ID:</strong>{created.gateway_id}</p>
|
||
<p><strong>API Key:</strong></p>
|
||
<Input.TextArea
|
||
readOnly
|
||
autoSize
|
||
value={created.api_key ?? ''}
|
||
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
||
/>
|
||
<p style={{ color: '#999', marginTop: 8 }}>请妥善保存 API Key,关闭后无法再次查看明文。</p>
|
||
</div>
|
||
),
|
||
width: 520,
|
||
});
|
||
}
|
||
|
||
setModalOpen(false);
|
||
fetchData(page, statusFilter);
|
||
} catch {
|
||
// validation
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (record: BleGateway) => {
|
||
try {
|
||
await bleGatewayApi.delete(record.gateway_id, record.version);
|
||
message.success('网关已删除');
|
||
fetchData(page, statusFilter);
|
||
} catch {
|
||
message.error('删除失败');
|
||
}
|
||
};
|
||
|
||
const handleRegenerateKey = async (record: BleGateway) => {
|
||
try {
|
||
const updated = await bleGatewayApi.regenerateKey(record.gateway_id);
|
||
Modal.success({
|
||
title: 'API Key 已重新生成',
|
||
content: (
|
||
<div>
|
||
<p><strong>新 API Key:</strong></p>
|
||
<Input.TextArea
|
||
readOnly
|
||
autoSize
|
||
value={updated.api_key ?? ''}
|
||
style={{ fontFamily: 'monospace', fontSize: 12 }}
|
||
/>
|
||
<p style={{ color: '#999', marginTop: 8 }}>旧 Key 已失效,请更新网关配置。</p>
|
||
</div>
|
||
),
|
||
width: 520,
|
||
});
|
||
fetchData(page, statusFilter);
|
||
} catch {
|
||
message.error('重新生成密钥失败');
|
||
}
|
||
};
|
||
|
||
const columns: ColumnsType<BleGateway> = useMemo(() => [
|
||
{
|
||
title: '网关名称',
|
||
dataIndex: 'name',
|
||
width: 180,
|
||
render: (v: string, record) => (
|
||
<a onClick={() => navigate(`/health/ble-gateways/${record.gateway_id}`)}>{v}</a>
|
||
),
|
||
},
|
||
{
|
||
title: '网关 ID',
|
||
dataIndex: 'gateway_id',
|
||
width: 160,
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
width: 100,
|
||
render: (v: string) => (
|
||
<Tag color={GATEWAY_STATUS_COLOR[v] ?? 'default'}>
|
||
{GATEWAY_STATUS_LABEL[v] ?? v}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '固件版本',
|
||
dataIndex: 'firmware_version',
|
||
width: 110,
|
||
render: (v: string) => v ?? '-',
|
||
},
|
||
{
|
||
title: 'IP 地址',
|
||
dataIndex: 'ip_address',
|
||
width: 130,
|
||
render: (v: string) => v ?? '-',
|
||
},
|
||
{
|
||
title: '绑定患者',
|
||
dataIndex: 'patient_count',
|
||
width: 90,
|
||
render: (v: number) => v ?? 0,
|
||
},
|
||
{
|
||
title: '最后心跳',
|
||
dataIndex: 'last_heartbeat_at',
|
||
width: 170,
|
||
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-',
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 220,
|
||
render: (_, record) => (
|
||
<Space>
|
||
<Button size="small" onClick={() => navigate(`/health/ble-gateways/${record.gateway_id}`)}>详情</Button>
|
||
<Button size="small" onClick={() => handleEdit(record)}>编辑</Button>
|
||
<Popconfirm title="确定重新生成 API Key?旧 Key 将立即失效。" onConfirm={() => handleRegenerateKey(record)}>
|
||
<Button size="small">刷新密钥</Button>
|
||
</Popconfirm>
|
||
<Popconfirm title="确定删除此网关?" onConfirm={() => handleDelete(record)}>
|
||
<Button size="small" danger>删除</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
], [page, statusFilter, navigate]);
|
||
|
||
if (!hasPermission) {
|
||
return (
|
||
<PageContainer title="BLE 网关管理">
|
||
<div style={{ textAlign: 'center', padding: 80, color: '#999' }}>权限不足</div>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PageContainer
|
||
title="BLE 网关管理"
|
||
actions={<Button type="primary" onClick={handleCreate}>添加网关</Button>}
|
||
>
|
||
<Space style={{ marginBottom: 16 }} wrap>
|
||
<Select
|
||
placeholder="状态筛选"
|
||
options={GATEWAY_STATUS_OPTIONS}
|
||
value={statusFilter}
|
||
onChange={setStatusFilter}
|
||
allowClear
|
||
style={{ width: 150 }}
|
||
/>
|
||
<Button type="primary" onClick={handleSearch}>查询</Button>
|
||
</Space>
|
||
|
||
<Table<BleGateway>
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={data}
|
||
loading={loading}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total,
|
||
showTotal: (t) => `共 ${t} 条`,
|
||
onChange: (p) => fetchData(p, statusFilter),
|
||
}}
|
||
/>
|
||
|
||
<Modal
|
||
title={editRecord ? '编辑网关' : '添加网关'}
|
||
open={modalOpen}
|
||
onOk={handleSubmit}
|
||
onCancel={() => setModalOpen(false)}
|
||
confirmLoading={submitting}
|
||
width={520}
|
||
>
|
||
<Form form={form} layout="vertical">
|
||
{!editRecord && (
|
||
<Form.Item name="gateway_id" label="网关 ID" rules={[{ required: true, message: '请输入网关 ID' }]}>
|
||
<Input placeholder="如:GW-001" />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item name="name" label="网关名称" rules={[{ required: true, message: '请输入网关名称' }]}>
|
||
<Input placeholder="如:透析室 1 号网关" />
|
||
</Form.Item>
|
||
{editRecord && (
|
||
<Form.Item name="status" label="状态">
|
||
<Select options={GATEWAY_STATUS_OPTIONS} placeholder="选择状态" />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item name="firmware_version" label="固件版本">
|
||
<Input placeholder="如:v2.1.0" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</PageContainer>
|
||
);
|
||
}
|