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:
@@ -1,12 +1,8 @@
|
||||
import { api } from './request'
|
||||
|
||||
export interface IndicatorDetail {
|
||||
value: number; unit?: string; reference_min?: number; reference_max?: number; status?: string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -40,10 +40,10 @@ export interface LabReport {
|
||||
patient_id: string;
|
||||
report_date: string;
|
||||
report_type: string;
|
||||
indicators?: Record<string, unknown>;
|
||||
items?: unknown;
|
||||
image_urls?: string[];
|
||||
doctor_notes?: string;
|
||||
doctor_interpretation?: string;
|
||||
source?: string;
|
||||
status: string;
|
||||
reviewed_by?: string;
|
||||
reviewed_at?: string;
|
||||
@@ -55,9 +55,9 @@ export interface LabReport {
|
||||
export interface CreateLabReportReq {
|
||||
report_date: string;
|
||||
report_type: string;
|
||||
indicators?: Record<string, unknown>;
|
||||
items?: unknown;
|
||||
image_urls?: string[];
|
||||
doctor_interpretation?: string;
|
||||
doctor_notes?: string;
|
||||
}
|
||||
|
||||
export interface HealthRecord {
|
||||
@@ -65,8 +65,10 @@ export interface HealthRecord {
|
||||
patient_id: string;
|
||||
record_type: string;
|
||||
record_date: string;
|
||||
content?: string;
|
||||
attachment_urls?: string[];
|
||||
overall_assessment?: string;
|
||||
report_file_url?: string;
|
||||
source?: string;
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: number;
|
||||
@@ -75,8 +77,8 @@ export interface HealthRecord {
|
||||
export interface CreateHealthRecordReq {
|
||||
record_type: string;
|
||||
record_date: string;
|
||||
content?: string;
|
||||
attachment_urls?: string[];
|
||||
overall_assessment?: string;
|
||||
report_file_url?: string;
|
||||
}
|
||||
|
||||
export interface DailyMonitoring {
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface MedicationReminder {
|
||||
medication_name: string;
|
||||
dosage?: string;
|
||||
frequency: string;
|
||||
time_slots: string[];
|
||||
reminder_times: unknown;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
is_active: boolean;
|
||||
@@ -24,7 +24,7 @@ export interface CreateMedicationReminderReq {
|
||||
medication_name: string;
|
||||
dosage?: string;
|
||||
frequency?: string;
|
||||
time_slots?: string[];
|
||||
reminder_times?: unknown;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
is_active?: boolean;
|
||||
@@ -35,7 +35,7 @@ export interface UpdateMedicationReminderReq {
|
||||
medication_name?: string;
|
||||
dosage?: string;
|
||||
frequency?: string;
|
||||
time_slots?: string[];
|
||||
reminder_times?: unknown;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
is_active?: boolean;
|
||||
|
||||
@@ -53,7 +53,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
form.setFieldsValue({
|
||||
record_type: record.record_type,
|
||||
record_date: dayjs(record.record_date),
|
||||
content: record.content,
|
||||
overall_assessment: record.overall_assessment,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -61,7 +61,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
const handleSubmit = async (values: {
|
||||
record_type: 'checkup' | 'outpatient' | 'inpatient';
|
||||
record_date: Dayjs;
|
||||
content?: string;
|
||||
overall_assessment?: string;
|
||||
}) => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
@@ -69,7 +69,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
await healthDataApi.updateHealthRecord(patientId, editingRecord.id, {
|
||||
record_type: values.record_type,
|
||||
record_date: values.record_date.format('YYYY-MM-DD'),
|
||||
content: values.content,
|
||||
overall_assessment: values.overall_assessment,
|
||||
version: editingRecord.version,
|
||||
});
|
||||
message.success('健康记录更新成功');
|
||||
@@ -77,7 +77,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
await healthDataApi.createHealthRecord(patientId, {
|
||||
record_type: values.record_type,
|
||||
record_date: values.record_date.format('YYYY-MM-DD'),
|
||||
content: values.content,
|
||||
overall_assessment: values.overall_assessment,
|
||||
});
|
||||
message.success('健康记录添加成功');
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
const columns = useMemo(() => [
|
||||
{ title: '记录类型', dataIndex: 'record_type', key: 'record_type', width: 120, render: (v: string) => <Tag>{RECORD_TYPE_MAP[v] || v}</Tag> },
|
||||
{ title: '记录日期', dataIndex: 'record_date', key: 'record_date', width: 120 },
|
||||
{ title: '内容', dataIndex: 'content', key: 'content', ellipsis: true },
|
||||
{ title: '内容', dataIndex: 'overall_assessment', key: 'overall_assessment', ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: (v: string) => new Date(v).toLocaleString('zh-CN') },
|
||||
{
|
||||
title: '操作',
|
||||
@@ -177,7 +177,7 @@ export function HealthRecordsTab({ patientId }: Props) {
|
||||
<Form.Item name="record_date" label="记录日期" rules={[{ required: true, message: '请选择日期' }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="内容">
|
||||
<Form.Item name="overall_assessment" label="内容">
|
||||
<Input.TextArea rows={4} placeholder="健康记录详细内容" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
form.setFieldsValue({
|
||||
report_date: dayjs(record.report_date),
|
||||
report_type: record.report_type,
|
||||
doctor_interpretation: record.doctor_interpretation,
|
||||
doctor_notes: record.doctor_notes,
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -87,7 +87,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
const handleSubmit = async (values: {
|
||||
report_date: Dayjs;
|
||||
report_type: string;
|
||||
doctor_interpretation?: string;
|
||||
doctor_notes?: string;
|
||||
}) => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
@@ -95,7 +95,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
await healthDataApi.updateLabReport(patientId, editingRecord.id, {
|
||||
report_date: values.report_date.format('YYYY-MM-DD'),
|
||||
report_type: values.report_type,
|
||||
doctor_interpretation: values.doctor_interpretation,
|
||||
doctor_notes: values.doctor_notes,
|
||||
version: editingRecord.version,
|
||||
});
|
||||
message.success('化验报告更新成功');
|
||||
@@ -103,7 +103,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
await healthDataApi.createLabReport(patientId, {
|
||||
report_date: values.report_date.format('YYYY-MM-DD'),
|
||||
report_type: values.report_type,
|
||||
doctor_interpretation: values.doctor_interpretation,
|
||||
doctor_notes: values.doctor_notes,
|
||||
});
|
||||
message.success('化验报告添加成功');
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '医生解读', dataIndex: 'doctor_interpretation', key: 'doctor_interpretation', ellipsis: true },
|
||||
{ title: '医生解读', dataIndex: 'doctor_notes', key: 'doctor_notes', ellipsis: true },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: (v: string) => new Date(v).toLocaleString('zh-CN') },
|
||||
{
|
||||
title: '操作',
|
||||
@@ -248,7 +248,7 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
<Form.Item name="report_type" label="报告类型" rules={[{ required: true, message: '请选择类型' }]}>
|
||||
<Input placeholder="如:血常规、生化全套" />
|
||||
</Form.Item>
|
||||
<Form.Item name="doctor_interpretation" label="医生解读">
|
||||
<Form.Item name="doctor_notes" label="医生解读">
|
||||
<Input.TextArea rows={3} placeholder="检查结果解读备注" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -271,9 +271,9 @@ export function LabReportsTab({ patientId }: Props) {
|
||||
<p style={{ marginBottom: 8 }}>
|
||||
<strong>报告日期:</strong>{reviewRecord.report_date}
|
||||
</p>
|
||||
{reviewRecord.doctor_interpretation && (
|
||||
{reviewRecord.doctor_notes && (
|
||||
<p style={{ marginBottom: 8 }}>
|
||||
<strong>医生解读:</strong>{reviewRecord.doctor_interpretation}
|
||||
<strong>医生解读:</strong>{reviewRecord.doctor_notes}
|
||||
</p>
|
||||
)}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
|
||||
Reference in New Issue
Block a user