feat(web): 家庭健康代理 + 知情同意 Web UI — Phase 2c
家庭代理:关联患者列表 + 健康摘要查看 + 授权/撤销访问 知情同意:患者范围 CRUD 列表页(类型/范围/签署/撤销)
This commit is contained in:
@@ -62,6 +62,8 @@ const BleGatewayList = lazy(() => import('./pages/health/BleGatewayList'));
|
||||
const BleGatewayDetail = lazy(() => import('./pages/health/BleGatewayDetail'));
|
||||
const CriticalValueThresholdList = lazy(() => import('./pages/health/CriticalValueThresholdList'));
|
||||
const DiagnosisList = lazy(() => import('./pages/health/DiagnosisList'));
|
||||
const FamilyProxyPage = lazy(() => import('./pages/health/FamilyProxyPage'));
|
||||
const ConsentList = lazy(() => import('./pages/health/ConsentList'));
|
||||
|
||||
// 内容管理
|
||||
const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList'));
|
||||
@@ -289,6 +291,8 @@ export default function App() {
|
||||
<Route path="/health/ble-gateways/:id" element={<BleGatewayDetail />} />
|
||||
<Route path="/health/critical-value-thresholds" element={<CriticalValueThresholdList />} />
|
||||
<Route path="/health/diagnoses" element={<DiagnosisList />} />
|
||||
<Route path="/health/family-proxy" element={<FamilyProxyPage />} />
|
||||
<Route path="/health/consents" element={<ConsentList />} />
|
||||
{/* 内容管理 */}
|
||||
<Route path="/health/articles" element={<ArticleManageList />} />
|
||||
<Route path="/health/articles/new" element={<ArticleEditor />} />
|
||||
|
||||
92
apps/web/src/api/health/consents.ts
Normal file
92
apps/web/src/api/health/consents.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import client from '../client';
|
||||
import type { PaginatedResponse } from '../types';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface Consent {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
consent_type: string;
|
||||
consent_scope: string;
|
||||
status: string;
|
||||
granted_at?: string;
|
||||
revoked_at?: string;
|
||||
expiry_date?: string;
|
||||
consent_method?: string;
|
||||
witness_name?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface CreateConsentReq {
|
||||
patient_id: string;
|
||||
consent_type: string;
|
||||
consent_scope: string;
|
||||
expiry_date?: string;
|
||||
consent_method?: string;
|
||||
witness_name?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface RevokeConsentReq {
|
||||
notes?: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
export const CONSENT_TYPE_OPTIONS = [
|
||||
{ label: '治疗同意', value: 'treatment' },
|
||||
{ label: '数据共享', value: 'data_sharing' },
|
||||
{ label: '隐私政策', value: 'privacy' },
|
||||
{ label: '研究参与', value: 'research' },
|
||||
];
|
||||
|
||||
export const CONSENT_SCOPE_OPTIONS = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '健康数据', value: 'health_data' },
|
||||
{ label: '基本信息', value: 'basic_info' },
|
||||
{ label: '体检报告', value: 'examination' },
|
||||
];
|
||||
|
||||
export const CONSENT_STATUS_COLOR: Record<string, string> = {
|
||||
active: 'green',
|
||||
revoked: 'red',
|
||||
expired: 'default',
|
||||
};
|
||||
|
||||
export const CONSENT_STATUS_LABEL: Record<string, string> = {
|
||||
active: '生效中',
|
||||
revoked: '已撤销',
|
||||
expired: '已过期',
|
||||
};
|
||||
|
||||
// --- API ---
|
||||
|
||||
export const consentApi = {
|
||||
list: async (patientId: string, params?: { page?: number; page_size?: number }) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PaginatedResponse<Consent>;
|
||||
}>(`/health/patients/${patientId}/consents`, { params });
|
||||
return data.data;
|
||||
},
|
||||
|
||||
grant: async (req: CreateConsentReq) => {
|
||||
const { data } = await client.post<{
|
||||
success: boolean;
|
||||
data: Consent;
|
||||
}>('/health/consents', req);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
revoke: async (consentId: string, req: RevokeConsentReq) => {
|
||||
const { data } = await client.put<{
|
||||
success: boolean;
|
||||
data: Consent;
|
||||
}>(`/health/consents/${consentId}/revoke`, req);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
109
apps/web/src/api/health/familyProxy.ts
Normal file
109
apps/web/src/api/health/familyProxy.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import client from '../client';
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface FamilyMember {
|
||||
id: string;
|
||||
patient_id: string;
|
||||
name: string;
|
||||
relationship: string;
|
||||
phone?: string;
|
||||
birth_date?: string;
|
||||
notes?: string;
|
||||
user_id?: string;
|
||||
consent_status: string;
|
||||
access_level: string;
|
||||
consented_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface FamilyPatientSummary {
|
||||
family_member_id: string;
|
||||
patient_id: string;
|
||||
patient_name: string;
|
||||
relationship: string;
|
||||
consent_status: string;
|
||||
access_level: string;
|
||||
consented_at?: string;
|
||||
}
|
||||
|
||||
export interface FamilyHealthSummary {
|
||||
patient_id: string;
|
||||
patient_name: string;
|
||||
latest_vital_signs?: Record<string, unknown>;
|
||||
active_care_plan?: Record<string, unknown>;
|
||||
recent_alerts_count: number;
|
||||
next_appointment?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GrantAccessReq {
|
||||
access_level: string;
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
export const CONSENT_STATUS_OPTIONS = [
|
||||
{ label: '已同意', value: 'granted' },
|
||||
{ label: '待确认', value: 'pending' },
|
||||
{ label: '已撤销', value: 'revoked' },
|
||||
{ label: '已过期', value: 'expired' },
|
||||
];
|
||||
|
||||
export const ACCESS_LEVEL_OPTIONS = [
|
||||
{ label: '完全访问', value: 'full' },
|
||||
{ label: '只读', value: 'read_only' },
|
||||
{ label: '摘要', value: 'summary' },
|
||||
];
|
||||
|
||||
export const CONSENT_STATUS_COLOR: Record<string, string> = {
|
||||
granted: 'green',
|
||||
pending: 'orange',
|
||||
revoked: 'red',
|
||||
expired: 'default',
|
||||
};
|
||||
|
||||
export const ACCESS_LEVEL_LABEL: Record<string, string> = Object.fromEntries(
|
||||
ACCESS_LEVEL_OPTIONS.map((o) => [o.value, o.label]),
|
||||
);
|
||||
|
||||
export const CONSENT_STATUS_LABEL: Record<string, string> = Object.fromEntries(
|
||||
CONSENT_STATUS_OPTIONS.map((o) => [o.value, o.label]),
|
||||
);
|
||||
|
||||
// --- API ---
|
||||
|
||||
export const familyProxyApi = {
|
||||
grantAccess: async (patientId: string, familyMemberId: string, req: GrantAccessReq, version: number) => {
|
||||
const { data } = await client.post<{
|
||||
success: boolean;
|
||||
data: FamilyMember;
|
||||
}>(`/health/patients/${patientId}/family-members/${familyMemberId}/grant-access?version=${version}`, req);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
revokeAccess: async (patientId: string, familyMemberId: string, version: number) => {
|
||||
const { data } = await client.put<{
|
||||
success: boolean;
|
||||
data: FamilyMember;
|
||||
}>(`/health/patients/${patientId}/family-members/${familyMemberId}/revoke-access?version=${version}`);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
listMyPatients: async () => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: FamilyPatientSummary[];
|
||||
}>('/health/family/my-patients');
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getHealthSummary: async (patientId: string) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: FamilyHealthSummary;
|
||||
}>(`/health/family/patients/${patientId}/health-summary`);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
@@ -120,6 +120,8 @@ const routeTitleFallback: Record<string, string> = {
|
||||
'/health/ble-gateways/:id': '网关详情',
|
||||
'/health/critical-value-thresholds': '危急值阈值',
|
||||
'/health/diagnoses': '诊断记录',
|
||||
'/health/family-proxy': '家庭健康代理',
|
||||
'/health/consents': '知情同意管理',
|
||||
};
|
||||
|
||||
function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined {
|
||||
|
||||
249
apps/web/src/pages/health/ConsentList.tsx
Normal file
249
apps/web/src/pages/health/ConsentList.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button, DatePicker, Form, Input, message, Modal, Popconfirm,
|
||||
Result, Select, Space, Table, Tag,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
consentApi,
|
||||
type Consent,
|
||||
type CreateConsentReq,
|
||||
CONSENT_TYPE_OPTIONS,
|
||||
CONSENT_SCOPE_OPTIONS,
|
||||
CONSENT_STATUS_COLOR,
|
||||
CONSENT_STATUS_LABEL,
|
||||
} from '../../api/health/consents';
|
||||
import { PageContainer } from '../../components/PageContainer';
|
||||
import { usePermission } from '../../hooks/usePermission';
|
||||
|
||||
export default function ConsentList() {
|
||||
const { hasPermission } = usePermission('health.consent.list');
|
||||
|
||||
const [data, setData] = useState<Consent[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [patientId, setPatientId] = useState('');
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
const fetchData = useCallback(async (pid: string, p: number) => {
|
||||
if (!pid) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await consentApi.list(pid, { page: p, page_size: pageSize });
|
||||
setData(resp.data);
|
||||
setTotal(resp.total);
|
||||
setPage(p);
|
||||
} catch {
|
||||
message.error('加载知情同意记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
const pid = searchInput.trim();
|
||||
if (!pid) {
|
||||
message.warning('请输入患者 ID');
|
||||
return;
|
||||
}
|
||||
setPatientId(pid);
|
||||
fetchData(pid, 1);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ patient_id: patientId });
|
||||
setCreateModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
const req: CreateConsentReq = {
|
||||
...values,
|
||||
expiry_date: values.expiry_date?.format('YYYY-MM-DD'),
|
||||
};
|
||||
await consentApi.grant(req);
|
||||
message.success('知情同意已签署');
|
||||
setCreateModalOpen(false);
|
||||
fetchData(patientId, page);
|
||||
} catch {
|
||||
// validation
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevoke = async (record: Consent) => {
|
||||
try {
|
||||
await consentApi.revoke(record.id, { version: record.version });
|
||||
message.success('知情同意已撤销');
|
||||
fetchData(patientId, page);
|
||||
} catch {
|
||||
message.error('撤销失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Consent> = useMemo(() => [
|
||||
{
|
||||
title: '同意类型',
|
||||
dataIndex: 'consent_type',
|
||||
width: 110,
|
||||
render: (v: string) => {
|
||||
const label = CONSENT_TYPE_OPTIONS.find((o) => o.value === v)?.label;
|
||||
return label ?? v;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '同意范围',
|
||||
dataIndex: 'consent_scope',
|
||||
width: 110,
|
||||
render: (v: string) => {
|
||||
const label = CONSENT_SCOPE_OPTIONS.find((o) => o.value === v)?.label;
|
||||
return label ?? v;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (v: string) => (
|
||||
<Tag color={CONSENT_STATUS_COLOR[v] ?? 'default'}>{CONSENT_STATUS_LABEL[v] ?? v}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '签署方式',
|
||||
dataIndex: 'consent_method',
|
||||
width: 100,
|
||||
render: (v: string) => v ?? '-',
|
||||
},
|
||||
{
|
||||
title: '见证人',
|
||||
dataIndex: 'witness_name',
|
||||
width: 100,
|
||||
render: (v: string) => v ?? '-',
|
||||
},
|
||||
{
|
||||
title: '签署时间',
|
||||
dataIndex: 'granted_at',
|
||||
width: 170,
|
||||
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-',
|
||||
},
|
||||
{
|
||||
title: '到期日',
|
||||
dataIndex: 'expiry_date',
|
||||
width: 110,
|
||||
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '无限期',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
record.status === 'active' ? (
|
||||
<Popconfirm title="确定撤销此知情同意?" onConfirm={() => handleRevoke(record)}>
|
||||
<Button size="small" danger>撤销</Button>
|
||||
</Popconfirm>
|
||||
) : null
|
||||
),
|
||||
},
|
||||
], [patientId, page]);
|
||||
|
||||
if (!hasPermission) {
|
||||
return <Result status="403" title="权限不足" subTitle="您没有查看知情同意记录的权限" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
title="知情同意管理"
|
||||
actions={patientId ? <Button type="primary" onClick={handleCreate}>签署同意</Button> : undefined}
|
||||
>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input.Search
|
||||
placeholder="输入患者 ID 搜索"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 360 }}
|
||||
enterButton="查询"
|
||||
/>
|
||||
{patientId && (
|
||||
<Tag color="blue">当前患者: {patientId}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{!patientId ? (
|
||||
<Result title="请输入患者 ID 查询知情同意记录" />
|
||||
) : (
|
||||
<Table<Consent>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange: (p) => fetchData(patientId, p),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="签署知情同意"
|
||||
open={createModalOpen}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setCreateModalOpen(false)}
|
||||
confirmLoading={submitting}
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="patient_id" label="患者 ID" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="consent_type" label="同意类型" rules={[{ required: true, message: '请选择类型' }]} style={{ width: 240 }}>
|
||||
<Select options={CONSENT_TYPE_OPTIONS} placeholder="选择类型" />
|
||||
</Form.Item>
|
||||
<Form.Item name="consent_scope" label="同意范围" rules={[{ required: true, message: '请选择范围' }]} style={{ width: 240 }}>
|
||||
<Select options={CONSENT_SCOPE_OPTIONS} placeholder="选择范围" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Space style={{ width: '100%' }} size="middle">
|
||||
<Form.Item name="consent_method" label="签署方式" style={{ width: 240 }}>
|
||||
<Select
|
||||
placeholder="选择方式"
|
||||
allowClear
|
||||
options={[
|
||||
{ label: '纸质签署', value: 'paper' },
|
||||
{ label: '电子签署', value: 'electronic' },
|
||||
{ label: '口头同意', value: 'verbal' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="expiry_date" label="到期日期" style={{ width: 240 }}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="witness_name" label="见证人">
|
||||
<Input placeholder="见证人姓名(可选)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="notes" label="备注">
|
||||
<Input.TextArea rows={2} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
211
apps/web/src/pages/health/FamilyProxyPage.tsx
Normal file
211
apps/web/src/pages/health/FamilyProxyPage.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user