feat(miniprogram): 医护工作台角色定制 + 性能优化
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- auth store 新增 health_manager 角色,添加 isDoctor/isNurse/isHealthManager/hasRole 辅助方法
- 医生工作台按角色过滤功能卡片和快捷操作(doctor/nurse/health_manager/admin)
- 列表页面分页计算提取为 useMemo(patients/alerts/consultation)
This commit is contained in:
iven
2026-05-06 12:51:00 +08:00
parent 570377a31f
commit 80ef48a3a3
5 changed files with 94 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { View, Text, ScrollView } from '@tarojs/components'; import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor'; import * as doctorApi from '@/services/doctor';
@@ -34,6 +34,8 @@ export default function AlertList() {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const totalPages = useMemo(() => totalPages, [total]);
useEffect(() => { useEffect(() => {
loadAlerts(); loadAlerts();
}, [page, activeTab]); }, [page, activeTab]);
@@ -137,11 +139,11 @@ export default function AlertList() {
</Text> </Text>
<Text className='alert-pagination__info'> <Text className='alert-pagination__info'>
{page} / {Math.ceil(total / 20)} {page} / {totalPages}
</Text> </Text>
<Text <Text
className={`alert-pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`} className={`alert-pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)} onClick={() => page < totalPages && setPage(page + 1)}
> >
</Text> </Text>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { View, Text, ScrollView } from '@tarojs/components'; import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor'; import * as doctorApi from '@/services/doctor';
@@ -22,6 +22,8 @@ export default function ConsultationList() {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const totalPages = useMemo(() => totalPages, [total]);
useEffect(() => { useEffect(() => {
loadSessions(); loadSessions();
}, [page, activeTab]); }, [page, activeTab]);
@@ -112,10 +114,10 @@ export default function ConsultationList() {
className={`pagination__btn ${page <= 1 ? 'disabled' : ''}`} className={`pagination__btn ${page <= 1 ? 'disabled' : ''}`}
onClick={() => page > 1 && setPage(page - 1)} onClick={() => page > 1 && setPage(page - 1)}
></Text> ></Text>
<Text className='pagination__info'>{page} / {Math.ceil(total / 20)}</Text> <Text className='pagination__info'>{page} / {totalPages}</Text>
<Text <Text
className={`pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`} className={`pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)} onClick={() => page < totalPages && setPage(page + 1)}
></Text> ></Text>
</View> </View>
)} )}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { View, Text, Input, ScrollView } from '@tarojs/components'; import { View, Text, Input, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
@@ -11,33 +11,66 @@ interface CardConfig {
label: string; label: string;
initial: string; initial: string;
route: string; route: string;
roles?: string[];
} }
const CARDS: CardConfig[] = [ const ALL_CARDS: CardConfig[] = [
{ key: 'total_patients', label: '我的患者', initial: '患', route: '/pages/doctor/patients/index' }, { key: 'total_patients', label: '我的患者', initial: '患', route: '/pages/doctor/patients/index' },
{ key: 'unread_messages', label: '未读消息', initial: '消', route: '/pages/doctor/consultation/index' }, { key: 'unread_messages', label: '未读消息', initial: '消', route: '/pages/doctor/consultation/index' },
{ key: 'pending_follow_ups', label: '待处理随访', initial: '随', route: '/pages/doctor/followup/index' }, { key: 'pending_follow_ups', label: '待处理随访', initial: '随', route: '/pages/doctor/followup/index', roles: ['doctor', 'nurse', 'health_manager'] },
{ key: 'today_consultations', label: '今日咨询', initial: '诊', route: '/pages/doctor/consultation/index' }, { key: 'today_consultations', label: '今日咨询', initial: '诊', route: '/pages/doctor/consultation/index', roles: ['doctor', 'health_manager'] },
]; ];
const HEALTH_CARDS: CardConfig[] = [ const ALL_HEALTH_CARDS: CardConfig[] = [
{ key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index' }, { key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index', roles: ['doctor'] },
{ key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/doctor/patients/index' }, { key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/doctor/patients/index' },
]; ];
const QUICK_ACTIONS = [ interface QuickAction {
{ label: '化验审核', initial: '审', route: '/pages/doctor/report/index' }, label: string;
{ label: '患者查询', initial: '查', route: '/pages/doctor/patients/index' }, initial: string;
{ label: '随访记录', initial: '随', route: '/pages/doctor/followup/index' }, route: string;
{ label: '告警中心', initial: '警', route: '/pages/doctor/alerts/index' }, roles: string[];
}
const ALL_QUICK_ACTIONS: QuickAction[] = [
{ label: '化验审核', initial: '审', route: '/pages/doctor/report/index', roles: ['doctor'] },
{ label: '患者查询', initial: '查', route: '/pages/doctor/patients/index', roles: ['doctor', 'nurse', 'health_manager'] },
{ label: '随访记录', initial: '随', route: '/pages/doctor/followup/index', roles: ['doctor', 'nurse', 'health_manager'] },
{ label: '告警中心', initial: '警', route: '/pages/doctor/alerts/index', roles: ['doctor', 'nurse', 'health_manager'] },
{ label: '透析管理', initial: '透', route: '/pages/doctor/dialysis/index', roles: ['doctor'] },
{ label: '处方管理', initial: '方', route: '/pages/doctor/prescription/index', roles: ['doctor'] },
{ label: '行动收件箱', initial: '行', route: '/pages/doctor/action-inbox/index', roles: ['doctor', 'nurse', 'health_manager'] },
]; ];
const ROLE_LABELS: Record<string, string> = {
doctor: '医生',
nurse: '护士',
health_manager: '健康管理师',
admin: '管理员',
operator: '运营',
};
export default function DoctorHome() { export default function DoctorHome() {
const { user, logout } = useAuthStore(); const { user, logout, roles } = useAuthStore();
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null); const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
const [alertCount, setAlertCount] = useState(0); const [alertCount, setAlertCount] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const hasRole = (allowed: string[] | undefined) => {
if (!allowed) return true;
return roles.some((r) => r === 'admin' || allowed.includes(r));
};
const cards = useMemo(() => ALL_CARDS.filter((c) => hasRole(c.roles)), [roles]);
const healthCards = useMemo(() => ALL_HEALTH_CARDS.filter((c) => hasRole(c.roles)), [roles]);
const quickActions = useMemo(() => ALL_QUICK_ACTIONS.filter((a) => hasRole(a.roles)), [roles]);
const roleLabel = useMemo(() => {
const primary = roles.find((r) => r !== 'admin');
return primary ? (ROLE_LABELS[primary] || primary) : '医护';
}, [roles]);
useEffect(() => { useEffect(() => {
loadDashboard(); loadDashboard();
}, []); }, []);
@@ -76,7 +109,7 @@ export default function DoctorHome() {
<View className='doctor-home__header'> <View className='doctor-home__header'>
<Text className='doctor-home__title'></Text> <Text className='doctor-home__title'></Text>
<Text className='doctor-home__greeting'> <Text className='doctor-home__greeting'>
{user?.display_name || user?.username || '医生'} {user?.display_name || user?.username || roleLabel}
</Text> </Text>
<Text className='doctor-home__date'> <Text className='doctor-home__date'>
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' })} {new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' })}
@@ -102,7 +135,7 @@ export default function DoctorHome() {
<View className='doctor-home__section'> <View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text> <Text className='doctor-home__section-title'></Text>
<View className='doctor-home__grid'> <View className='doctor-home__grid'>
{CARDS.map((card) => ( {cards.map((card) => (
<View <View
key={card.key} key={card.key}
className='doctor-home__card' className='doctor-home__card'
@@ -116,10 +149,10 @@ export default function DoctorHome() {
</View> </View>
</View> </View>
<View className='doctor-home__section'> {healthCards.length > 0 && (<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text> <Text className='doctor-home__section-title'></Text>
<View className='doctor-home__grid'> <View className='doctor-home__grid'>
{HEALTH_CARDS.map((card) => ( {healthCards.map((card) => (
<View <View
key={card.key} key={card.key}
className='doctor-home__card' className='doctor-home__card'
@@ -131,12 +164,12 @@ export default function DoctorHome() {
</View> </View>
))} ))}
</View> </View>
</View> </View>)}
<View className='doctor-home__section'> <View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text> <Text className='doctor-home__section-title'></Text>
<View className='doctor-home__quick-actions'> <View className='doctor-home__quick-actions'>
{QUICK_ACTIONS.map((action) => ( {quickActions.map((action) => (
<View <View
key={action.route} key={action.route}
className='quick-action' className='quick-action'

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { View, Text, Input, ScrollView } from '@tarojs/components'; import { View, Text, Input, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor'; import * as doctorApi from '@/services/doctor';
@@ -75,6 +75,8 @@ export default function PatientList() {
return `${age}`; return `${age}`;
}; };
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
if (loading && patients.length === 0) return <Loading />; if (loading && patients.length === 0) return <Loading />;
return ( return (
@@ -162,10 +164,10 @@ export default function PatientList() {
> >
</Text> </Text>
<Text className='pagination__info'>{page} / {Math.ceil(total / 20)}</Text> <Text className='pagination__info'>{page} / {totalPages}</Text>
<Text <Text
className={`pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`} className={`pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)} onClick={() => page < totalPages && setPage(page + 1)}
> >
</Text> </Text>

View File

@@ -24,6 +24,10 @@ interface AuthState {
logout: () => void; logout: () => void;
restore: () => void; restore: () => void;
isMedicalStaff: () => boolean; isMedicalStaff: () => boolean;
isDoctor: () => boolean;
isNurse: () => boolean;
isHealthManager: () => boolean;
hasRole: (code: string) => boolean;
hasPatientProfile: () => boolean; hasPatientProfile: () => boolean;
} }
@@ -36,7 +40,27 @@ export const useAuthStore = create<AuthState>((set, get) => ({
isMedicalStaff: () => { isMedicalStaff: () => {
const { roles } = get(); const { roles } = get();
return roles.some((r) => r === 'doctor' || r === 'nurse' || r === 'admin'); return roles.some((r) => r === 'doctor' || r === 'nurse' || r === 'admin' || r === 'health_manager');
},
isDoctor: () => {
const { roles } = get();
return roles.some((r) => r === 'doctor' || r === 'admin');
},
isNurse: () => {
const { roles } = get();
return roles.some((r) => r === 'nurse' || r === 'admin');
},
isHealthManager: () => {
const { roles } = get();
return roles.some((r) => r === 'health_manager' || r === 'admin');
},
hasRole: (code: string) => {
const { roles } = get();
return roles.some((r) => r === code || r === 'admin');
}, },
hasPatientProfile: () => { hasPatientProfile: () => {