Files
hms/apps/web/src/pages/health/FamilyProxyPage.tsx
iven 888fa108ef
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): 家庭健康代理 + 知情同意 Web UI — Phase 2c
家庭代理:关联患者列表 + 健康摘要查看 + 授权/撤销访问
知情同意:患者范围 CRUD 列表页(类型/范围/签署/撤销)
2026-05-05 00:02:39 +08:00

212 lines
6.8 KiB
TypeScript

import { useState, useCallback } from 'react';
import {
Button, Card, Descriptions, Form, Input, message, Modal, Popconfirm, Result, Select, Space, Table, Tag,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import {
familyProxyApi,
type FamilyPatientSummary,
type FamilyHealthSummary,
ACCESS_LEVEL_OPTIONS,
ACCESS_LEVEL_LABEL,
CONSENT_STATUS_LABEL,
CONSENT_STATUS_COLOR,
} from '../../api/health/familyProxy';
import { PageContainer } from '../../components/PageContainer';
export default function FamilyProxyPage() {
const [patients, setPatients] = useState<FamilyPatientSummary[]>([]);
const [loading, setLoading] = useState(false);
const [loaded, setLoaded] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState<string | null>(null);
const [healthSummary, setHealthSummary] = useState<FamilyHealthSummary | null>(null);
const [summaryLoading, setSummaryLoading] = useState(false);
const [grantModalOpen, setGrantModalOpen] = useState(false);
const [grantTarget, setGrantTarget] = useState<FamilyPatientSummary | null>(null);
const [grantSubmitting, setGrantSubmitting] = useState(false);
const [grantForm] = Form.useForm();
const fetchPatients = useCallback(async () => {
setLoading(true);
try {
const list = await familyProxyApi.listMyPatients();
setPatients(list);
setLoaded(true);
} catch {
message.error('加载关联患者失败');
} finally {
setLoading(false);
}
}, []);
const handleViewSummary = async (patientId: string) => {
setSelectedPatientId(patientId);
setSummaryLoading(true);
try {
const summary = await familyProxyApi.getHealthSummary(patientId);
setHealthSummary(summary);
} catch {
message.error('加载健康摘要失败');
} finally {
setSummaryLoading(false);
}
};
const handleGrantAccess = (record: FamilyPatientSummary) => {
setGrantTarget(record);
grantForm.resetFields();
grantForm.setFieldsValue({ access_level: 'read_only' });
setGrantModalOpen(true);
};
const handleRevokeAccess = async (record: FamilyPatientSummary) => {
try {
await familyProxyApi.revokeAccess(record.patient_id, record.family_member_id, 0);
message.success('已撤销访问权限');
fetchPatients();
} catch {
message.error('撤销失败');
}
};
const handleSubmitGrant = async () => {
try {
const values = await grantForm.validateFields();
setGrantSubmitting(true);
await familyProxyApi.grantAccess(
grantTarget!.patient_id,
grantTarget!.family_member_id,
values,
0,
);
message.success('访问权限已授权');
setGrantModalOpen(false);
fetchPatients();
} catch {
// validation
} finally {
setGrantSubmitting(false);
}
};
const patientColumns: ColumnsType<FamilyPatientSummary> = [
{
title: '患者姓名',
dataIndex: 'patient_name',
width: 120,
},
{
title: '关系',
dataIndex: 'relationship',
width: 80,
},
{
title: '同意状态',
dataIndex: 'consent_status',
width: 100,
render: (v: string) => (
<Tag color={CONSENT_STATUS_COLOR[v] ?? 'default'}>{CONSENT_STATUS_LABEL[v] ?? v}</Tag>
),
},
{
title: '访问级别',
dataIndex: 'access_level',
width: 100,
render: (v: string) => ACCESS_LEVEL_LABEL[v] ?? v ?? '-',
},
{
title: '授权时间',
dataIndex: 'consented_at',
width: 170,
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-',
},
{
title: '操作',
width: 240,
render: (_, record) => (
<Space>
<Button size="small" onClick={() => handleViewSummary(record.patient_id)}>
</Button>
{record.consent_status !== 'granted' && (
<Button size="small" type="primary" onClick={() => handleGrantAccess(record)}>
访
</Button>
)}
{record.consent_status === 'granted' && (
<Popconfirm title="确定撤销访问权限?" onConfirm={() => handleRevokeAccess(record)}>
<Button size="small" danger></Button>
</Popconfirm>
)}
</Space>
),
},
];
return (
<PageContainer
title="家庭健康代理"
actions={<Button type="primary" onClick={fetchPatients} loading={loading}>{loaded ? '刷新' : '加载关联患者'}</Button>}
>
{loaded && patients.length === 0 ? (
<Result title="暂无关联患者" subTitle="您尚未被添加为任何患者的家庭成员" />
) : (
<>
<Table<FamilyPatientSummary>
rowKey="family_member_id"
columns={patientColumns}
dataSource={patients}
loading={loading}
pagination={false}
/>
{selectedPatientId && (
<Card title="健康摘要" style={{ marginTop: 24 }} loading={summaryLoading}>
{healthSummary && (
<Descriptions bordered column={2}>
<Descriptions.Item label="患者">{healthSummary.patient_name}</Descriptions.Item>
<Descriptions.Item label="近期告警数">{healthSummary.recent_alerts_count}</Descriptions.Item>
<Descriptions.Item label="最新体征" span={2}>
{healthSummary.latest_vital_signs
? JSON.stringify(healthSummary.latest_vital_signs, null, 2)
: '暂无数据'}
</Descriptions.Item>
<Descriptions.Item label="活跃护理计划" span={2}>
{healthSummary.active_care_plan
? JSON.stringify(healthSummary.active_care_plan, null, 2)
: '暂无'}
</Descriptions.Item>
<Descriptions.Item label="下次预约" span={2}>
{healthSummary.next_appointment
? JSON.stringify(healthSummary.next_appointment, null, 2)
: '暂无'}
</Descriptions.Item>
</Descriptions>
)}
</Card>
)}
</>
)}
<Modal
title={`授权访问 — ${grantTarget?.patient_name ?? ''}`}
open={grantModalOpen}
onOk={handleSubmitGrant}
onCancel={() => setGrantModalOpen(false)}
confirmLoading={grantSubmitting}
width={400}
>
<Form form={grantForm} layout="vertical">
<Form.Item name="access_level" label="访问级别" rules={[{ required: true }]}>
<Select options={ACCESS_LEVEL_OPTIONS} />
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
}