fix: 多角色业务链路测试发现并修复 3 类问题
1. 角色权限修复(CRITICAL): - operator 角色权限为空(迁移 name/code 不匹配 + 软删除冲突) - doctor 角色权限被误清(API assign_permissions 失败导致全部软删除) - nurse 缺 devices 权限 + doctor/nurse 缺 appointment 权限 - 新增 3 个迁移 000130-000132 修复所有角色权限 2. 趋势指标映射修复(HIGH): - 前端 blood_pressure_systolic → systolic_bp_morning - 前端 blood_sugar_fasting → blood_sugar - 同步修复首页、健康页、趋势页的 indicator 参数 3. 咨询页错误处理优化(MEDIUM): - 403/401 时显示空列表而非"加载失败"错误提示
This commit is contained in:
@@ -8,73 +8,100 @@ import Loading from '../../components/Loading';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis';
|
||||
import { notificationService } from '@/services/notification';
|
||||
import './index.scss';
|
||||
|
||||
interface UpcomingItem {
|
||||
interface ReminderItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
type: 'appointment' | 'followup';
|
||||
icon: string;
|
||||
text: string;
|
||||
type: 'ai' | 'appointment' | 'followup';
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const { user, currentPatient } = useAuthStore();
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
|
||||
const [upcomingLoading, setUpcomingLoading] = useState(false);
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
refreshToday();
|
||||
loadUpcoming();
|
||||
loadReminders();
|
||||
loadUnread();
|
||||
trackPageView('home');
|
||||
});
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
Promise.all([refreshToday(true), loadUpcoming()]).finally(() => {
|
||||
Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => {
|
||||
Taro.stopPullDownRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
const loadUpcoming = async () => {
|
||||
const loadUnread = async () => {
|
||||
try {
|
||||
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
|
||||
const d = res.data;
|
||||
setUnreadCount(typeof d === 'object' && d ? (d.count ?? 0) : 0);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const loadReminders = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId) return;
|
||||
setUpcomingLoading(true);
|
||||
setRemindersLoading(true);
|
||||
try {
|
||||
const items: UpcomingItem[] = [];
|
||||
const [apptRes, taskRes] = await Promise.allSettled([
|
||||
const items: ReminderItem[] = [];
|
||||
|
||||
const [apptRes, taskRes, suggestRes] = await Promise.allSettled([
|
||||
appointmentApi.listAppointments(patientId, 1),
|
||||
followupApi.listTasks(patientId, 'pending'),
|
||||
listPendingSuggestions(),
|
||||
]);
|
||||
|
||||
if (suggestRes.status === 'fulfilled') {
|
||||
for (const s of suggestRes.value.data.slice(0, 1)) {
|
||||
items.push({
|
||||
id: s.id,
|
||||
text: buildSuggestionText(s),
|
||||
type: 'ai',
|
||||
tag: 'AI 建议',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (apptRes.status === 'fulfilled') {
|
||||
for (const a of apptRes.value.data.slice(0, 2)) {
|
||||
for (const a of apptRes.value.data.slice(0, 1)) {
|
||||
if (a.status === 'pending' || a.status === 'confirmed') {
|
||||
items.push({
|
||||
id: a.id,
|
||||
title: `${a.appointment_date} ${a.start_time}`,
|
||||
subtitle: `${a.doctor_name || '医护'} · ${a.department || '门诊'}`,
|
||||
text: `${a.appointment_date} ${a.start_time} — ${a.doctor_name || '医护'} ${a.department || '门诊'}`,
|
||||
type: 'appointment',
|
||||
icon: '约',
|
||||
tag: '预约',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (taskRes.status === 'fulfilled') {
|
||||
for (const t of taskRes.value.data.slice(0, 1)) {
|
||||
items.push({
|
||||
id: t.id,
|
||||
title: t.follow_up_type,
|
||||
subtitle: `${t.content_template?.slice(0, 20) || '随访任务'} · 截止 ${t.planned_date}`,
|
||||
text: `${t.follow_up_type} · 截止 ${t.planned_date}`,
|
||||
type: 'followup',
|
||||
icon: '随',
|
||||
tag: '随访',
|
||||
});
|
||||
}
|
||||
}
|
||||
setUpcomingItems(items.slice(0, 3));
|
||||
|
||||
setReminders(items.slice(0, 3));
|
||||
} catch {
|
||||
setUpcomingItems([]);
|
||||
setReminders([]);
|
||||
} finally {
|
||||
setUpcomingLoading(false);
|
||||
setRemindersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,7 +109,6 @@ export default function Index() {
|
||||
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
|
||||
const displayName = user?.display_name || currentPatient?.name || '访客';
|
||||
|
||||
// 计算今日体征完成度(4 个指标:血压/心率/血糖/体重)
|
||||
const summary = todaySummary || {};
|
||||
const indicators = [
|
||||
!!summary.blood_pressure,
|
||||
@@ -101,9 +127,9 @@ export default function Index() {
|
||||
];
|
||||
|
||||
const healthItems = [
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'blood_pressure_systolic' },
|
||||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' },
|
||||
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
|
||||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar_fasting' },
|
||||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||||
];
|
||||
|
||||
@@ -115,7 +141,7 @@ export default function Index() {
|
||||
|
||||
return (
|
||||
<View className='home-page'>
|
||||
{/* 区域 1:问候 + 日期 + 消息入口 */}
|
||||
{/* 问候区 */}
|
||||
<View className='greeting-section'>
|
||||
<View className='greeting-left'>
|
||||
<Text className='greeting-text'>{greeting},{displayName}</Text>
|
||||
@@ -128,10 +154,11 @@ export default function Index() {
|
||||
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
|
||||
>
|
||||
<Text className='greeting-bell-icon'>消</Text>
|
||||
{unreadCount > 0 && <View className='greeting-bell-dot' />}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 区域 2:今日体征完成度 */}
|
||||
{/* 今日体征进度 */}
|
||||
<View
|
||||
className='checkin-card'
|
||||
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
|
||||
@@ -149,15 +176,16 @@ export default function Index() {
|
||||
key={cap.label}
|
||||
className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`}
|
||||
>
|
||||
{cap.done ? '✓' : ''}{cap.label}
|
||||
{cap.done ? '✓ ' : ''}{cap.label}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 区域 3:今日体征 2x2 网格 */}
|
||||
{/* 体征 2x2 */}
|
||||
<View className='vitals-section'>
|
||||
<Text className='section-title'>今日体征</Text>
|
||||
{loading && !todaySummary ? (
|
||||
<Loading />
|
||||
) : (
|
||||
@@ -186,44 +214,34 @@ export default function Index() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 区域 4:今日待办(≤3 条) */}
|
||||
<View className='todo-section'>
|
||||
<Text className='section-title'>今日待办</Text>
|
||||
{upcomingLoading ? (
|
||||
<Loading />
|
||||
) : upcomingItems.length === 0 ? (
|
||||
<View className='todo-empty'>
|
||||
<Text className='todo-empty-text'>今天没有待办事项</Text>
|
||||
{/* 智能提醒卡片 */}
|
||||
{!remindersLoading && reminders.length > 0 && (
|
||||
<View className='reminder-card'>
|
||||
<View className='reminder-header'>
|
||||
<Text className='reminder-title'>智能提醒</Text>
|
||||
<Text className='reminder-count'>{reminders.length} 条待处理</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='todo-list'>
|
||||
{upcomingItems.map((item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
className='todo-item'
|
||||
onClick={() => {
|
||||
if (item.type === 'appointment') {
|
||||
Taro.navigateTo({ url: '/pages/appointment/index' });
|
||||
} else {
|
||||
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${item.id}` });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View className='todo-icon-wrap'>
|
||||
<Text className='todo-icon-char'>{item.icon}</Text>
|
||||
</View>
|
||||
<View className='todo-info'>
|
||||
<Text className='todo-title'>{item.title}</Text>
|
||||
<Text className='todo-sub'>{item.subtitle}</Text>
|
||||
</View>
|
||||
<Text className='todo-arrow'>›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{reminders.map((r, i) => (
|
||||
<View
|
||||
key={r.id}
|
||||
className={`reminder-item ${i > 0 ? 'reminder-item-border' : ''}`}
|
||||
onClick={() => {
|
||||
if (r.type === 'appointment') {
|
||||
Taro.navigateTo({ url: '/pages/appointment/index' });
|
||||
} else if (r.type === 'followup') {
|
||||
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text className='reminder-tag'>{r.tag}</Text>
|
||||
<Text className='reminder-text'>{r.text}</Text>
|
||||
<Text className='reminder-arrow'>›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 区域 5:快捷操作 */}
|
||||
{/* 快捷操作 */}
|
||||
<View className='action-section'>
|
||||
<View
|
||||
className='action-btn action-primary'
|
||||
@@ -241,3 +259,20 @@ export default function Index() {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function buildSuggestionText(s: AiSuggestionItem): string {
|
||||
const riskMap: Record<string, string> = {
|
||||
high: '高风险',
|
||||
medium: '中风险',
|
||||
low: '低风险',
|
||||
};
|
||||
const typeMap: Record<string, string> = {
|
||||
vital_sign_anomaly: '体征异常',
|
||||
lab_result_anomaly: '化验异常',
|
||||
medication_adherence: '用药提醒',
|
||||
lifestyle: '生活建议',
|
||||
};
|
||||
const risk = riskMap[s.risk_level] || '';
|
||||
const type = typeMap[s.suggestion_type] || '健康建议';
|
||||
return `${type}:发现${risk}指标,建议关注`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user