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 Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor';
@@ -34,6 +34,8 @@ export default function AlertList() {
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const totalPages = useMemo(() => totalPages, [total]);
useEffect(() => {
loadAlerts();
}, [page, activeTab]);
@@ -137,11 +139,11 @@ export default function AlertList() {
</Text>
<Text className='alert-pagination__info'>
{page} / {Math.ceil(total / 20)}
{page} / {totalPages}
</Text>
<Text
className={`alert-pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)}
className={`alert-pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < totalPages && setPage(page + 1)}
>
</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 Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor';
@@ -22,6 +22,8 @@ export default function ConsultationList() {
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const totalPages = useMemo(() => totalPages, [total]);
useEffect(() => {
loadSessions();
}, [page, activeTab]);
@@ -112,10 +114,10 @@ export default function ConsultationList() {
className={`pagination__btn ${page <= 1 ? 'disabled' : ''}`}
onClick={() => page > 1 && setPage(page - 1)}
></Text>
<Text className='pagination__info'>{page} / {Math.ceil(total / 20)}</Text>
<Text className='pagination__info'>{page} / {totalPages}</Text>
<Text
className={`pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)}
className={`pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < totalPages && setPage(page + 1)}
></Text>
</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 Taro from '@tarojs/taro';
import { useAuthStore } from '@/stores/auth';
@@ -11,33 +11,66 @@ interface CardConfig {
label: string;
initial: string;
route: string;
roles?: string[];
}
const CARDS: CardConfig[] = [
const ALL_CARDS: CardConfig[] = [
{ key: 'total_patients', label: '我的患者', initial: '患', route: '/pages/doctor/patients/index' },
{ key: 'unread_messages', label: '未读消息', initial: '消', route: '/pages/doctor/consultation/index' },
{ key: 'pending_follow_ups', label: '待处理随访', initial: '随', route: '/pages/doctor/followup/index' },
{ key: 'today_consultations', label: '今日咨询', initial: '诊', route: '/pages/doctor/consultation/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', roles: ['doctor', 'health_manager'] },
];
const HEALTH_CARDS: CardConfig[] = [
{ key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index' },
const ALL_HEALTH_CARDS: CardConfig[] = [
{ key: 'pending_lab_review', label: '待审化验', initial: '化', route: '/pages/doctor/report/index', roles: ['doctor'] },
{ key: 'today_appointments', label: '今日预约', initial: '约', route: '/pages/doctor/patients/index' },
];
const QUICK_ACTIONS = [
{ label: '化验审核', initial: '审', route: '/pages/doctor/report/index' },
{ label: '患者查询', initial: '查', route: '/pages/doctor/patients/index' },
{ label: '随访记录', initial: '随', route: '/pages/doctor/followup/index' },
{ label: '告警中心', initial: '警', route: '/pages/doctor/alerts/index' },
interface QuickAction {
label: string;
initial: string;
route: string;
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() {
const { user, logout } = useAuthStore();
const { user, logout, roles } = useAuthStore();
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
const [alertCount, setAlertCount] = useState(0);
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(() => {
loadDashboard();
}, []);
@@ -76,7 +109,7 @@ export default function DoctorHome() {
<View className='doctor-home__header'>
<Text className='doctor-home__title'></Text>
<Text className='doctor-home__greeting'>
{user?.display_name || user?.username || '医生'}
{user?.display_name || user?.username || roleLabel}
</Text>
<Text className='doctor-home__date'>
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long' })}
@@ -102,7 +135,7 @@ export default function DoctorHome() {
<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text>
<View className='doctor-home__grid'>
{CARDS.map((card) => (
{cards.map((card) => (
<View
key={card.key}
className='doctor-home__card'
@@ -116,10 +149,10 @@ export default function DoctorHome() {
</View>
</View>
<View className='doctor-home__section'>
{healthCards.length > 0 && (<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text>
<View className='doctor-home__grid'>
{HEALTH_CARDS.map((card) => (
{healthCards.map((card) => (
<View
key={card.key}
className='doctor-home__card'
@@ -131,12 +164,12 @@ export default function DoctorHome() {
</View>
))}
</View>
</View>
</View>)}
<View className='doctor-home__section'>
<Text className='doctor-home__section-title'></Text>
<View className='doctor-home__quick-actions'>
{QUICK_ACTIONS.map((action) => (
{quickActions.map((action) => (
<View
key={action.route}
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 Taro from '@tarojs/taro';
import * as doctorApi from '@/services/doctor';
@@ -75,6 +75,8 @@ export default function PatientList() {
return `${age}`;
};
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
if (loading && patients.length === 0) return <Loading />;
return (
@@ -162,10 +164,10 @@ export default function PatientList() {
>
</Text>
<Text className='pagination__info'>{page} / {Math.ceil(total / 20)}</Text>
<Text className='pagination__info'>{page} / {totalPages}</Text>
<Text
className={`pagination__btn ${page >= Math.ceil(total / 20) ? 'disabled' : ''}`}
onClick={() => page < Math.ceil(total / 20) && setPage(page + 1)}
className={`pagination__btn ${page >= totalPages ? 'disabled' : ''}`}
onClick={() => page < totalPages && setPage(page + 1)}
>
</Text>

View File

@@ -24,6 +24,10 @@ interface AuthState {
logout: () => void;
restore: () => void;
isMedicalStaff: () => boolean;
isDoctor: () => boolean;
isNurse: () => boolean;
isHealthManager: () => boolean;
hasRole: (code: string) => boolean;
hasPatientProfile: () => boolean;
}
@@ -36,7 +40,27 @@ export const useAuthStore = create<AuthState>((set, get) => ({
isMedicalStaff: () => {
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: () => {