fix(mp): 修复小程序角色路由 + 前后端字段对齐 + E2E 测试报告

- 修复 stores/auth.ts 三种登录方式从错误路径提取 roles(resp.roles → resp.user.roles)
- 首页添加医护人员自动跳转医生端(useDidShow + isMedicalStaff)
- services/auth.ts credentialLogin 返回类型补全 roles 字段
- Web 前端 healthData.ts 字段对齐后端 DTO(indicators→items, content→overall_assessment)
- Web 前端 medicationReminders.ts 字段对齐(time_slots→reminder_times)
- 小程序 report.ts / reports 页面字段对齐后端(indicators→items, doctor_interpretation→doctor_notes)
- 小程序 patient.ts / followup.ts / alert.ts 补全缺失字段
- 后端 stats_handler.rs 权限码修正(health.patient.list→health.dashboard.manage)
- 新增 V1 E2E 测试报告和五专家组评审报告
This commit is contained in:
iven
2026-05-17 01:51:02 +08:00
parent aa27c5174c
commit c38967a36e
17 changed files with 898 additions and 67 deletions

View File

@@ -287,9 +287,17 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
export default function Index() {
const user = useAuthStore((s) => s.user);
const isMedicalStaff = useAuthStore((s) => s.isMedicalStaff);
const mode = useUIStore((s) => s.mode);
const modeClass = mode === 'elder' ? 'elder-mode' : '';
// 医护人员访问患者首页时,自动跳转到医生端
useDidShow(() => {
if (user && isMedicalStaff()) {
Taro.redirectTo({ url: '/pages/pkg-doctor-core/index' });
}
});
if (!user) {
return <GuestHome modeClass={modeClass} />;
}

View File

@@ -45,14 +45,15 @@ export default function ReportDetail() {
usePageData(fetchReport, { throttleMs: 60000 });
const indicators: IndicatorItem[] = React.useMemo(() => {
if (!report?.indicators || typeof report.indicators !== 'object') return [];
return Object.entries(report.indicators).map(([name, val]) => ({
name,
value: val.value,
unit: val.unit,
reference_min: val.reference_min,
reference_max: val.reference_max,
status: val.status,
if (!report?.items || typeof report.items !== 'object') return [];
const items = Array.isArray(report.items) ? report.items : [];
return items.map((val: Record<string, unknown>, idx: number) => ({
name: (val.name as string) || `指标 ${idx + 1}`,
value: val.value as number,
unit: val.unit as string,
reference_min: (val.reference_low ?? val.reference_min) as number | undefined,
reference_max: (val.reference_high ?? val.reference_max) as number | undefined,
status: val.is_abnormal ? '异常' : '正常',
}));
}, [report]);
@@ -89,10 +90,10 @@ export default function ReportDetail() {
<Text className='detail-label'></Text>
<Text className='detail-value'>{report.report_date}</Text>
</View>
{report.doctor_interpretation && (
{report.doctor_notes && (
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>{report.doctor_interpretation}</Text>
<Text className='detail-value'>{report.doctor_notes}</Text>
</View>
)}
</ContentCard>

View File

@@ -53,10 +53,10 @@ export default function MyReports() {
};
const formatStatus = (report: LabReport) => {
const indicators = report.indicators;
if (!indicators || typeof indicators !== 'object') return 'unknown';
const vals = Object.values(indicators) as Array<{ status?: string }>;
const hasAbnormal = vals.some((v) => v.status === 'high' || v.status === 'low');
const items = report.items;
if (!items || !Array.isArray(items)) return 'unknown';
const vals = items as Array<{ is_abnormal?: boolean }>;
const hasAbnormal = vals.some((v) => v.is_abnormal);
return hasAbnormal ? 'abnormal' : 'normal';
};

View File

@@ -2,13 +2,18 @@ import { api } from './request';
export interface Alert {
id: string;
patient_id?: string;
rule_id?: string;
severity: string;
title: string;
message?: string;
status: string;
detail?: Record<string, unknown>;
created_at: string;
updated_at?: string;
acknowledged_at?: string;
resolved_at?: string;
version?: number;
}
export async function listPatientAlerts(patientId: string, params?: { status?: string; page?: number; page_size?: number }) {

View File

@@ -28,7 +28,7 @@ export interface PatientInfo {
}
export async function credentialLogin(username: string, password: string, tenantId: string) {
return api.post<{ access_token: string; refresh_token: string; expires_in?: number; user: { id: string; username: string; display_name?: string; phone?: string; tenant_id?: string } }>('/auth/login', {
return api.post<{ access_token: string; refresh_token: string; expires_in?: number; user: { id: string; username: string; display_name?: string; phone?: string; tenant_id?: string; roles?: Array<{ code: string; name: string }> } }>('/auth/login', {
username,
password,
tenant_id: tenantId,

View File

@@ -6,8 +6,12 @@ export interface FollowUpTask {
patient_name?: string;
follow_up_type: string;
content_template?: string;
assigned_to?: string;
assigned_to_name?: string;
status: string;
planned_date: string;
completed_at?: string;
notes?: string;
version: number;
}

View File

@@ -5,9 +5,18 @@ export interface Patient {
name: string;
gender?: string;
birth_date?: string;
phone?: string;
blood_type?: string;
id_number?: string;
allergy_history?: string;
medical_history_summary?: string;
emergency_contact_name?: string;
emergency_contact_phone?: string;
phone?: string;
relation?: string;
status?: string;
verification_status?: string;
source?: string;
notes?: string;
version: number;
}

View File

@@ -12,8 +12,8 @@ export interface LabReport {
id: string;
report_date: string;
report_type: string;
indicators: Record<string, IndicatorDetail>;
doctor_interpretation?: string;
items?: unknown;
doctor_notes?: string;
image_urls?: string[];
version: number;
}

View File

@@ -131,8 +131,9 @@ export const useAuthStore = create<AuthState>((set, get) => ({
const resp = await authApi.wechatLogin(code);
if (resp.bound && resp.token) {
const { access_token, refresh_token, user } = resp.token;
const roles = (resp as Record<string, unknown>).roles instanceof Array
? ((resp as Record<string, unknown>).roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
const userObj = user as Record<string, unknown>;
const roles = Array.isArray(userObj?.roles)
? (userObj.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
: [];
secureSet('access_token', access_token);
secureSet('refresh_token', refresh_token);
@@ -161,19 +162,19 @@ export const useAuthStore = create<AuthState>((set, get) => ({
try {
const tenantId = Taro.getStorageSync('tenant_id') || process.env.TARO_APP_DEFAULT_TENANT_ID || '';
const resp = await authApi.credentialLogin(username, password, tenantId);
const roles = (resp as Record<string, unknown>).roles instanceof Array
? ((resp as Record<string, unknown>).roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
const user = resp.user as Record<string, unknown>;
const roles = Array.isArray(user?.roles)
? (user.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
: [];
secureSet('access_token', resp.access_token);
secureSet('refresh_token', resp.refresh_token);
if (resp.expires_in) {
secureSet('token_expires_at', String(Date.now() + resp.expires_in * 1000));
}
const user = resp.user;
secureSet('user_data', JSON.stringify(user));
secureSet('user_data', JSON.stringify(resp.user));
secureSet('user_roles', JSON.stringify(roles));
secureSet('tenant_id', user.tenant_id || tenantId);
set({ user, roles, loading: false });
secureSet('tenant_id', resp.user?.tenant_id || tenantId);
set({ user: resp.user, roles, loading: false });
clearLoggingOut();
return true;
} catch {
@@ -193,8 +194,9 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}
const resp = await authApi.wechatBindPhone(openid, encryptedData, iv) as Record<string, unknown>;
const tokenData = resp as { access_token: string; refresh_token: string; expires_in?: number; user: AuthState['user'] };
const roles = resp.roles instanceof Array
? (resp.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
const userObj = tokenData.user as Record<string, unknown>;
const roles = Array.isArray(userObj?.roles)
? (userObj.roles as Array<Record<string, string>>).map((r) => r.code || r.name || String(r))
: [];
secureSet('access_token', tokenData.access_token);
secureSet('refresh_token', tokenData.refresh_token);