diff --git a/apps/miniprogram/src/app.config.ts b/apps/miniprogram/src/app.config.ts index faa16e5..3b7b319 100644 --- a/apps/miniprogram/src/app.config.ts +++ b/apps/miniprogram/src/app.config.ts @@ -41,6 +41,7 @@ export default defineAppConfig({ 'followups/index', 'medication/index', 'settings/index', 'dialysis-records/index', 'dialysis-records/detail/index', 'dialysis-prescriptions/index', 'dialysis-prescriptions/detail/index', + 'consents/index', ], }, { diff --git a/apps/miniprogram/src/pages/pkg-profile/consents/index.scss b/apps/miniprogram/src/pages/pkg-profile/consents/index.scss new file mode 100644 index 0000000..da5f049 --- /dev/null +++ b/apps/miniprogram/src/pages/pkg-profile/consents/index.scss @@ -0,0 +1,102 @@ +@import '../../../styles/variables.scss'; + +@mixin section-title { + font-family: 'Georgia', 'Times New Roman', serif; + font-size: 30px; + font-weight: bold; + color: $tx; + margin-bottom: 20px; + display: block; +} + +@mixin tag($bg, $color) { + display: inline-block; + padding: 4px 12px; + border-radius: 8px; + font-size: 20px; + font-weight: 500; + background: $bg; + color: $color; +} + +.consents-page { + min-height: 100vh; + background: $bg; + padding: 32px 24px; + padding-bottom: 40px; +} + +.page-title { + @include section-title; + padding-left: 4px; +} + +.consent-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.consent-card { + background: $card; + border-radius: $r; + padding: 28px; + box-shadow: $shadow-sm; +} + +.consent-card__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.consent-card__type { + font-size: 28px; + font-weight: bold; + color: $tx; +} + +.status-tag { + @include tag($bd-l, $tx3); + + &.granted { + @include tag($acc-l, $acc); + } + + &.revoked { + @include tag($dan-l, $dan); + } +} + +.consent-card__scope, +.consent-card__date, +.consent-card__expiry { + font-size: 24px; + color: $tx2; + display: block; + margin-bottom: 4px; + font-variant-numeric: tabular-nums; +} + +.revoke-btn { + margin-top: 16px; + padding: 12px 0; + text-align: center; + border-radius: $r-sm; + border: 1px solid $dan; + + &:active { + background: $dan-l; + } + + &--disabled { + opacity: 0.5; + } +} + +.revoke-btn__text { + font-size: 24px; + color: $dan; + font-weight: 500; +} diff --git a/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx b/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx new file mode 100644 index 0000000..359cef5 --- /dev/null +++ b/apps/miniprogram/src/pages/pkg-profile/consents/index.tsx @@ -0,0 +1,125 @@ +import React, { useState, useCallback } from 'react'; +import { View, Text } from '@tarojs/components'; +import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro'; +import { listConsents, revokeConsent } from '@/services/consent'; +import type { Consent } from '@/services/consent'; +import EmptyState from '@/components/EmptyState'; +import Loading from '@/components/Loading'; +import './index.scss'; + +const CONSENT_TYPE_MAP: Record = { + data_processing: '数据处理同意', + health_data_collection: '健康数据采集', + research_use: '科研使用', + third_party_share: '第三方共享', + genetic_testing: '基因检测', + telemedicine: '远程医疗', +}; + +const STATUS_MAP: Record = { + granted: { label: '已签署', cls: 'granted' }, + revoked: { label: '已撤回', cls: 'revoked' }, +}; + +export default function ConsentList() { + const [consents, setConsents] = useState([]); + const [page, setPage] = useState(1); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [revoking, setRevoking] = useState(null); + + const fetchData = useCallback(async (p: number, append = false) => { + const patientId = Taro.getStorageSync('current_patient_id') || ''; + if (!patientId) { + setConsents([]); + return; + } + setLoading(true); + try { + const res = await listConsents(patientId, { page: p, page_size: 20 }); + const list = res.data || []; + setConsents(append ? (prev) => [...prev, ...list] : list); + setTotal(res.total); + setPage(p); + } catch { + Taro.showToast({ title: '加载失败', icon: 'none' }); + } finally { + setLoading(false); + } + }, []); + + useDidShow(() => { fetchData(1); }); + + usePullDownRefresh(() => { + fetchData(1).finally(() => Taro.stopPullDownRefresh()); + }); + + useReachBottom(() => { + if (!loading && consents.length < total) { + fetchData(page + 1, true); + } + }); + + const handleRevoke = async (consent: Consent) => { + const { confirm } = await Taro.showModal({ + title: '确认撤回', + content: `确定要撤回「${CONSENT_TYPE_MAP[consent.consent_type] || consent.consent_type}」的同意吗?`, + }); + if (!confirm) return; + setRevoking(consent.id); + try { + const updated = await revokeConsent(consent.id, consent.version); + setConsents((prev) => prev.map((c) => c.id === updated.id ? updated : c)); + Taro.showToast({ title: '已撤回', icon: 'success' }); + } catch { + Taro.showToast({ title: '撤回失败', icon: 'none' }); + } finally { + setRevoking(null); + } + }; + + return ( + + 知情同意 + + + {consents.map((c) => { + const si = STATUS_MAP[c.status] || { label: c.status, cls: '' }; + const typeName = CONSENT_TYPE_MAP[c.consent_type] || c.consent_type; + return ( + + + {typeName} + {si.label} + + 范围: {c.consent_scope} + {c.granted_at && ( + 签署时间: {c.granted_at} + )} + {c.revoked_at && ( + 撤回时间: {c.revoked_at} + )} + {c.expiry_date && ( + 有效期至: {c.expiry_date} + )} + {c.status === 'granted' && ( + handleRevoke(c)} + > + {revoking === c.id ? '处理中...' : '撤回同意'} + + )} + + ); + })} + + + {consents.length === 0 && !loading && ( + + )} + + {loading && } + + ); +} diff --git a/apps/miniprogram/src/pages/profile/index.tsx b/apps/miniprogram/src/pages/profile/index.tsx index 8fdc480..89fd6b8 100644 --- a/apps/miniprogram/src/pages/profile/index.tsx +++ b/apps/miniprogram/src/pages/profile/index.tsx @@ -14,6 +14,7 @@ const MENU_ITEMS = [ { label: '用药提醒', char: '药', path: '/pages/pkg-profile/medication/index' }, { label: '透析记录', char: '透', path: '/pages/pkg-profile/dialysis-records/index' }, { label: '透析处方', char: '方', path: '/pages/pkg-profile/dialysis-prescriptions/index' }, + { label: '知情同意', char: '知', path: '/pages/pkg-profile/consents/index' }, { label: '设置', char: '设', path: '/pages/pkg-profile/settings/index' }, ]; diff --git a/apps/miniprogram/src/services/consent.ts b/apps/miniprogram/src/services/consent.ts new file mode 100644 index 0000000..df1aefc --- /dev/null +++ b/apps/miniprogram/src/services/consent.ts @@ -0,0 +1,48 @@ +import { api } from './request'; + +// ── 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; +} + +// ── Patient-facing API ──────────────────────────────── + +export async function listConsents( + patientId: string, + params?: { page?: number; page_size?: number }, +) { + return api.get<{ data: Consent[]; total: number }>( + `/health/patients/${patientId}/consents`, + params, + ); +} + +export async function grantConsent(data: { + patient_id: string; + consent_type: string; + consent_scope: string; + expiry_date?: string; + consent_method?: string; + witness_name?: string; + notes?: string; +}) { + return api.post('/health/consents', data); +} + +export async function revokeConsent(id: string, version: number, notes?: string) { + return api.put(`/health/consents/${id}/revoke`, { version, notes }); +}