diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index ac3fb75..8f0c383 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -47,6 +47,7 @@ const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard')); const AlertList = lazy(() => import('./pages/health/AlertList')); const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard')); const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList')); +const DeviceManage = lazy(() => import('./pages/health/DeviceManage')); // 内容管理 const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList')); @@ -252,6 +253,7 @@ export default function App() { } /> } /> } /> + } /> {/* 内容管理 */} } /> } /> diff --git a/apps/web/src/api/health/devices.ts b/apps/web/src/api/health/devices.ts new file mode 100644 index 0000000..c6347b4 --- /dev/null +++ b/apps/web/src/api/health/devices.ts @@ -0,0 +1,32 @@ +import client from '../client'; +import type { PaginatedResponse } from '../types'; + +// --- Types --- +export interface DeviceItem { + id: string; + patient_id: string; + device_id: string; + device_model: string; + device_type: string; + bound_at: string; + last_sync_at: string; + version: number; +} + +// --- API --- +export const deviceApi = { + listDevices: (params?: { + patient_id?: string; + device_type?: string; + page?: number; + page_size?: number; + }) => + client + .get('/health/devices', { params }) + .then((r) => r.data.data as PaginatedResponse), + + unbindDevice: (id: string, version: number) => + client + .delete(`/health/devices/${id}`, { data: { version } }) + .then((r) => r.data.data as DeviceItem), +}; diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index 5162bae..ab41509 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -94,6 +94,7 @@ const routeTitleFallback: Record = { '/health/alerts': '告警列表', '/health/alert-dashboard': '告警仪表盘', '/health/alert-rules': '告警规则', + '/health/devices': '设备管理', }; function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined { diff --git a/apps/web/src/pages/health/DeviceManage.tsx b/apps/web/src/pages/health/DeviceManage.tsx new file mode 100644 index 0000000..42982f7 --- /dev/null +++ b/apps/web/src/pages/health/DeviceManage.tsx @@ -0,0 +1,164 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Button, Input, message, Popconfirm, Select, Space, Table, Tag } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import dayjs from 'dayjs'; + +import { deviceApi, type DeviceItem } from '../../api/health/devices'; + +const DEVICE_TYPE_OPTIONS = [ + { label: '血压', value: 'blood_pressure' }, + { label: '血糖', value: 'blood_glucose' }, + { label: '心率', value: 'heart_rate' }, + { label: '血氧', value: 'blood_oxygen' }, + { label: '步数', value: 'steps' }, + { label: '体温', value: 'temperature' }, +]; + +const DEVICE_TYPE_COLOR: Record = { + blood_pressure: 'red', + blood_glucose: 'purple', + heart_rate: 'volcano', + blood_oxygen: 'blue', + steps: 'green', + temperature: 'orange', +}; + +function formatTime(val?: string | null): string { + if (!val) return '-'; + return dayjs(val).format('YYYY-MM-DD HH:mm'); +} + +export default function DeviceManage() { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + + const [filterPatientId, setFilterPatientId] = useState(''); + const [filterDeviceType, setFilterDeviceType] = useState(undefined); + + const fetchDevices = useCallback(async () => { + setLoading(true); + try { + const res = await deviceApi.listDevices({ + page, + page_size: 20, + ...(filterPatientId ? { patient_id: filterPatientId } : {}), + ...(filterDeviceType ? { device_type: filterDeviceType } : {}), + }); + setData(res.data); + setTotal(res.total); + } catch { + message.error('加载设备列表失败'); + } finally { + setLoading(false); + } + }, [page, filterPatientId, filterDeviceType]); + + useEffect(() => { + fetchDevices(); + }, [fetchDevices]); + + const handleUnbind = async (record: DeviceItem) => { + try { + await deviceApi.unbindDevice(record.id, record.version); + message.success('设备已解绑'); + fetchDevices(); + } catch { + message.error('解绑失败'); + } + }; + + const columns: ColumnsType = [ + { + title: '设备 ID', + dataIndex: 'device_id', + width: 120, + render: (v: string) => v.slice(0, 8), + }, + { + title: '设备型号', + dataIndex: 'device_model', + width: 160, + }, + { + title: '设备类型', + dataIndex: 'device_type', + width: 100, + render: (v: string) => { + const label = DEVICE_TYPE_OPTIONS.find((d) => d.value === v)?.label || v; + return {label}; + }, + }, + { + title: '绑定时间', + dataIndex: 'bound_at', + width: 170, + render: (v: string) => formatTime(v), + }, + { + title: '最后同步', + dataIndex: 'last_sync_at', + width: 170, + render: (v: string) => formatTime(v), + }, + { + title: '操作', + width: 80, + render: (_, record) => ( + handleUnbind(record)} + okText="确定" + cancelText="取消" + > + + + ), + }, + ]; + + return ( +
+

设备管理

+ + + setFilterPatientId(e.target.value)} + style={{ width: 200 }} + allowClear + /> + { + setDeviceType(v); + refresh(1); + }} + /> + { + setDeviceType(v); + refresh(1); + }} + /> +