Files
hms/apps/web/src/pages/health/BleGatewayDetail.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

255 lines
7.8 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, useEffect } from 'react';
import {
Button, Descriptions, Form, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tabs,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { useParams, useNavigate } from 'react-router-dom';
import {
bleGatewayApi,
type BleGateway,
type GatewayBinding,
type CreateBindingReq,
GATEWAY_STATUS_LABEL,
GATEWAY_STATUS_COLOR,
BINDING_STATUS_COLOR,
} from '../../api/health/bleGateways';
import { PageContainer } from '../../components/PageContainer';
export default function BleGatewayDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [gateway, setGateway] = useState<BleGateway | null>(null);
const [loading, setLoading] = useState(false);
// Bindings state
const [bindings, setBindings] = useState<GatewayBinding[]>([]);
const [bindingsTotal, setBindingsTotal] = useState(0);
const [bindingsPage, setBindingsPage] = useState(1);
const [bindingsLoading, setBindingsLoading] = useState(false);
// Binding modal
const [bindModalOpen, setBindModalOpen] = useState(false);
const [bindSubmitting, setBindSubmitting] = useState(false);
const [bindForm] = Form.useForm();
const pageSize = 20;
const fetchGateway = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const resp = await bleGatewayApi.get(id);
setGateway(resp);
} catch {
message.error('加载网关详情失败');
} finally {
setLoading(false);
}
}, [id]);
const fetchBindings = useCallback(async (p: number) => {
if (!id) return;
setBindingsLoading(true);
try {
const resp = await bleGatewayApi.listBindings(id, { page: p, page_size: pageSize });
setBindings(resp.data);
setBindingsTotal(resp.total);
setBindingsPage(p);
} catch {
message.error('加载绑定列表失败');
} finally {
setBindingsLoading(false);
}
}, [id]);
useEffect(() => {
fetchGateway();
fetchBindings(1);
}, [fetchGateway, fetchBindings]);
const handleBindPatient = () => {
bindForm.resetFields();
setBindModalOpen(true);
};
const handleSubmitBind = async () => {
try {
const values = await bindForm.validateFields();
setBindSubmitting(true);
const req: CreateBindingReq = values;
await bleGatewayApi.bindPatient(id!, req);
message.success('患者绑定成功');
setBindModalOpen(false);
fetchBindings(bindingsPage);
fetchGateway();
} catch {
// validation
} finally {
setBindSubmitting(false);
}
};
const handleUnbind = async (binding: GatewayBinding) => {
try {
await bleGatewayApi.unbindPatient(id!, binding.id, binding.version);
message.success('已解除绑定');
fetchBindings(bindingsPage);
fetchGateway();
} catch {
message.error('解除绑定失败');
}
};
const bindingColumns: ColumnsType<GatewayBinding> = [
{
title: '患者 ID',
dataIndex: 'patient_id',
width: 200,
ellipsis: true,
},
{
title: '外设 MAC',
dataIndex: 'peripheral_mac',
width: 160,
render: (v: string) => v ?? '-',
},
{
title: '设备类型',
dataIndex: 'device_type',
width: 120,
render: (v: string) => v ?? '-',
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (v: string) => (
<Tag color={BINDING_STATUS_COLOR[v] ?? 'default'}>{v}</Tag>
),
},
{
title: '绑定时间',
dataIndex: 'created_at',
width: 170,
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-',
},
{
title: '操作',
width: 100,
render: (_, record) => (
<Popconfirm title="确定解除此患者绑定?" onConfirm={() => handleUnbind(record)}>
<Button size="small" danger></Button>
</Popconfirm>
),
},
];
if (!gateway && !loading) {
return (
<PageContainer title="网关详情" onBack={() => navigate('/health/ble-gateways')}>
<div style={{ textAlign: 'center', padding: 80, color: '#999' }}></div>
</PageContainer>
);
}
return (
<PageContainer
title={gateway?.name ?? '网关详情'}
onBack={() => navigate('/health/ble-gateways')}
loading={loading}
>
<Descriptions bordered column={2} style={{ marginBottom: 24 }}>
<Descriptions.Item label="网关 ID">{gateway?.gateway_id}</Descriptions.Item>
<Descriptions.Item label="网关名称">{gateway?.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={GATEWAY_STATUS_COLOR[gateway?.status ?? ''] ?? 'default'}>
{GATEWAY_STATUS_LABEL[gateway?.status ?? ''] ?? gateway?.status}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="固件版本">{gateway?.firmware_version ?? '-'}</Descriptions.Item>
<Descriptions.Item label="IP 地址">{gateway?.ip_address ?? '-'}</Descriptions.Item>
<Descriptions.Item label="绑定患者数">{gateway?.patient_count ?? 0}</Descriptions.Item>
<Descriptions.Item label="最后心跳">
{gateway?.last_heartbeat_at
? dayjs(gateway.last_heartbeat_at).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{gateway?.created_at
? dayjs(gateway.created_at).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</Descriptions.Item>
</Descriptions>
<Tabs
defaultActiveKey="bindings"
items={[
{
key: 'bindings',
label: '设备绑定',
children: (
<>
<Button type="primary" style={{ marginBottom: 16 }} onClick={handleBindPatient}>
</Button>
<Table<GatewayBinding>
rowKey="id"
columns={bindingColumns}
dataSource={bindings}
loading={bindingsLoading}
pagination={{
current: bindingsPage,
pageSize,
total: bindingsTotal,
showTotal: (t) => `${t}`,
onChange: (p) => fetchBindings(p),
}}
/>
</>
),
},
]}
/>
<Modal
title="绑定患者"
open={bindModalOpen}
onOk={handleSubmitBind}
onCancel={() => setBindModalOpen(false)}
confirmLoading={bindSubmitting}
width={480}
>
<Form form={bindForm} layout="vertical">
<Form.Item
name="patient_id"
label="患者 ID"
rules={[{ required: true, message: '请输入患者 ID' }]}
>
<Input placeholder="输入患者 ID" />
</Form.Item>
<Form.Item name="peripheral_mac" label="外设 MAC 地址">
<Input placeholder="如AA:BB:CC:DD:EE:FF" />
</Form.Item>
<Form.Item name="device_type" label="设备类型">
<Select
placeholder="选择设备类型"
allowClear
options={[
{ label: '血压计', value: 'blood_pressure' },
{ label: '血氧仪', value: 'pulse_oximeter' },
{ label: '体温计', value: 'thermometer' },
{ label: '体重秤', value: 'weight_scale' },
{ label: '血糖仪', value: 'glucometer' },
{ label: '心电仪', value: 'ecg' },
]}
/>
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
}