feat(auth,mp): 患者登录流程优化 — 智能合并 + 角色冻结 + 页面冻结

- 智能合并:微信注册时用手机号盲索引匹配已有患者档案,避免重复建
  档(AuthState 添加 PiiCrypto + ensure_patient_record 增加盲索引查询)
- 角色冻结:小程序仅允许患者角色登录,医护角色被拦截
  (auth_service.rs 添加反向拦截 + 登录页移除 credential login 表单)
- 页面冻结:10 个非核心页面替换为 FrozenPage 占位组件(用药/知情同意
  /透析/家属/诊断/事件),移除 profile 导航入口,移除医生端预加载
- 医生端代码保留,仅隐藏入口,后续可零成本恢复

讨论记录:docs/discussions/2026-05-23-account-registration-login-flow.md
This commit is contained in:
iven
2026-05-23 12:27:14 +08:00
parent f7d98a59f0
commit f11dd59382
21 changed files with 328 additions and 1510 deletions

View File

@@ -1,131 +1,5 @@
import { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useReachBottom } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { getCachedPatientId } from '@/services/request';
import { listConsents, revokeConsent } from '@/services/consent';
import type { Consent } from '@/services/consent';
import EmptyState from '@/components/EmptyState';
import Loading from '@/components/Loading';
import { useElderClass } from '../../../hooks/useElderClass';
import PageShell from '@/components/ui/PageShell';
import './index.scss';
import FrozenPage from '@/components/FrozenPage';
const CONSENT_TYPE_MAP: Record<string, string> = {
data_processing: '数据处理同意',
health_data_collection: '健康数据采集',
research_use: '科研使用',
third_party_share: '第三方共享',
genetic_testing: '基因检测',
telemedicine: '远程医疗',
};
const STATUS_MAP: Record<string, { label: string; cls: string }> = {
granted: { label: '已签署', cls: 'granted' },
revoked: { label: '已撤回', cls: 'revoked' },
};
export default function ConsentList() {
const modeClass = useElderClass();
const [consents, setConsents] = useState<Consent[]>([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [revoking, setRevoking] = useState<string | null>(null);
const [hasPatient, setHasPatient] = useState(true);
const fetchData = useCallback(async (p: number, append = false) => {
const patientId = getCachedPatientId();
if (!patientId) {
setConsents([]);
setHasPatient(false);
return;
}
setHasPatient(true);
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 (err) {
console.warn('[consent] 加载失败:', err);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
}, []);
usePageData(async () => { await fetchData(1); }, { throttleMs: 10000, enablePullDown: true });
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 (err) {
console.warn('[consent] 撤回失败:', err);
Taro.showToast({ title: '撤回失败', icon: 'none' });
} finally {
setRevoking(null);
}
};
return (
<PageShell className={modeClass}>
<Text className='page-title'></Text>
<View className='consent-list'>
{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 (
<View className='consent-card' key={c.id}>
<View className='consent-card__header'>
<Text className='consent-card__type'>{typeName}</Text>
<Text className={`status-tag ${si.cls}`}>{si.label}</Text>
</View>
<Text className='consent-card__scope'>: {c.consent_scope}</Text>
{c.granted_at && (
<Text className='consent-card__date'>: {c.granted_at}</Text>
)}
{c.revoked_at && (
<Text className='consent-card__date'>: {c.revoked_at}</Text>
)}
{c.expiry_date && (
<Text className='consent-card__expiry'>: {c.expiry_date}</Text>
)}
{c.status === 'granted' && (
<View
className={`revoke-btn ${revoking === c.id ? 'revoke-btn--disabled' : ''}`}
onClick={() => handleRevoke(c)}
>
<Text className='revoke-btn__text'>{revoking === c.id ? '处理中...' : '撤回同意'}</Text>
</View>
)}
</View>
);
})}
</View>
{consents.length === 0 && !loading && (
<EmptyState text={hasPatient ? '暂无知情同意记录' : '请先在就诊人管理中选择就诊人'} />
)}
{loading && <Loading />}
</PageShell>
);
export default function ConsentsPage() {
return <FrozenPage />;
}