Files
hms/apps/web/src/pages/health/BleGatewayList.tsx
iven b6838c1bc1
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(web): BLE 网关管理 UI — Phase 2b-1
网关 CRUD 列表页(状态筛选/密钥刷新/API Key 创建展示)
+ 网关详情页(信息面板/设备绑定管理 Tab)
2026-05-04 23:47:21 +08:00

287 lines
8.3 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 { 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>
);
}