fix(miniprogram): 首页体征数据加载时序 + 并发控制 + 权限修复
- ConcurrencyLimiter 12→8 预留长轮询通道,避免超微信 10 并发限制 - usePageData 添加 AbortController,页面隐藏/卸载自动取消请求 - useHomeData 添加 useEffect 监听 currentPatient 变化自动触发数据加载 - 医护人员首页跳转前不渲染 HomeDashboard,避免触发无用 API 请求 - auth.ts getPatients 正确提取分页响应 .data 数组 - health.ts getTodaySummary 从 Storage 回退读取 patient_id - health store refreshToday 从 auth store 回退获取 currentPatient.id - auth store restore() 状态变化时清理请求缓存,避免返回过期数据
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useRef, useState, useCallback } from 'react';
|
import { useRef, useState, useCallback, useEffect } from 'react';
|
||||||
import Taro, { useDidShow, usePullDownRefresh } from '@tarojs/taro';
|
import Taro, { useDidShow, useDidHide, usePullDownRefresh } from '@tarojs/taro';
|
||||||
|
|
||||||
interface UsePageDataOptions {
|
interface UsePageDataOptions {
|
||||||
throttleMs?: number;
|
throttleMs?: number;
|
||||||
@@ -14,7 +14,7 @@ interface UsePageDataResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePageData(
|
export function usePageData(
|
||||||
fetcher: () => Promise<void>,
|
fetcher: (signal?: AbortSignal) => Promise<void>,
|
||||||
options?: UsePageDataOptions,
|
options?: UsePageDataOptions,
|
||||||
): UsePageDataResult {
|
): UsePageDataResult {
|
||||||
const throttleMs = options?.throttleMs ?? 5000;
|
const throttleMs = options?.throttleMs ?? 5000;
|
||||||
@@ -26,6 +26,14 @@ export function usePageData(
|
|||||||
const lastRunRef = useRef(0);
|
const lastRunRef = useRef(0);
|
||||||
const fetcherRef = useRef(fetcher);
|
const fetcherRef = useRef(fetcher);
|
||||||
fetcherRef.current = fetcher;
|
fetcherRef.current = fetcher;
|
||||||
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const abort = useCallback(() => {
|
||||||
|
if (abortRef.current) {
|
||||||
|
abortRef.current.abort();
|
||||||
|
abortRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const run = useCallback(async (force = false) => {
|
const run = useCallback(async (force = false) => {
|
||||||
if (!enabled || loadingRef.current) return;
|
if (!enabled || loadingRef.current) return;
|
||||||
@@ -33,11 +41,15 @@ export function usePageData(
|
|||||||
loadingRef.current = true;
|
loadingRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
lastRunRef.current = Date.now();
|
lastRunRef.current = Date.now();
|
||||||
|
abort();
|
||||||
|
const ac = new AbortController();
|
||||||
|
abortRef.current = ac;
|
||||||
try {
|
try {
|
||||||
await fetcherRef.current();
|
await fetcherRef.current(ac.signal);
|
||||||
} finally {
|
} finally {
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
if (abortRef.current === ac) abortRef.current = null;
|
||||||
}
|
}
|
||||||
}, [enabled, throttleMs]);
|
}, [enabled, throttleMs]);
|
||||||
|
|
||||||
@@ -45,6 +57,16 @@ export function usePageData(
|
|||||||
run();
|
run();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useDidHide(() => {
|
||||||
|
abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
abort();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const trigger = useCallback(() => {
|
const trigger = useCallback(() => {
|
||||||
run(true);
|
run(true);
|
||||||
}, [run]);
|
}, [run]);
|
||||||
@@ -54,11 +76,15 @@ export function usePageData(
|
|||||||
loadingRef.current = true;
|
loadingRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
lastRunRef.current = Date.now();
|
lastRunRef.current = Date.now();
|
||||||
|
abort();
|
||||||
|
const ac = new AbortController();
|
||||||
|
abortRef.current = ac;
|
||||||
try {
|
try {
|
||||||
await fetcherRef.current();
|
await fetcherRef.current(ac.signal);
|
||||||
} finally {
|
} finally {
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
if (abortRef.current === ac) abortRef.current = null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -314,14 +314,25 @@ export default function Index() {
|
|||||||
const modeClass = mode === 'elder' ? 'elder-mode' : '';
|
const modeClass = mode === 'elder' ? 'elder-mode' : '';
|
||||||
|
|
||||||
// 医护人员访问患者首页时,自动跳转到医生端
|
// 医护人员访问患者首页时,自动跳转到医生端
|
||||||
|
// 不渲染 HomeDashboard,避免触发患者首页的 API 请求(并发叠加问题)
|
||||||
|
const shouldRedirect = user && isMedicalStaff();
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
if (user && isMedicalStaff()) {
|
if (shouldRedirect) {
|
||||||
Taro.redirectTo({ url: '/pages/pkg-doctor-core/index' });
|
Taro.reLaunch({
|
||||||
|
url: '/pages/pkg-doctor-core/index',
|
||||||
|
fail: () => {
|
||||||
|
console.warn('跳转医生端失败,停留患者首页');
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <GuestHome modeClass={modeClass} />;
|
return <GuestHome modeClass={modeClass} />;
|
||||||
}
|
}
|
||||||
|
if (shouldRedirect) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
return <HomeDashboard modeClass={modeClass} />;
|
return <HomeDashboard modeClass={modeClass} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useRef } from 'react';
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
import { useHealthStore } from '@/stores/health';
|
import { useHealthStore } from '@/stores/health';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { usePageData } from '@/hooks/usePageData';
|
import { usePageData } from '@/hooks/usePageData';
|
||||||
@@ -53,6 +53,17 @@ export function useHomeData() {
|
|||||||
enabled: !!user,
|
enabled: !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// currentPatient 从 null 变为有值时重新触发加载
|
||||||
|
// 解决 loadPatients 异步完成前 useDidShow 已触发 fetchData 并因 patientId 为空提前返回的问题
|
||||||
|
const prevPatientRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const pid = currentPatient?.id ?? null;
|
||||||
|
if (pid && pid !== prevPatientRef.current) {
|
||||||
|
prevPatientRef.current = pid;
|
||||||
|
trigger();
|
||||||
|
}
|
||||||
|
}, [currentPatient?.id, trigger]);
|
||||||
|
|
||||||
const loadUnread = async () => {
|
const loadUnread = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
|
const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number };
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export async function wechatBindPhone(openid: string, encryptedData: string, iv:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPatients() {
|
interface PaginatedData<T> {
|
||||||
return api.get<PatientInfo[]>('/health/patients');
|
data: T[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPatients() {
|
||||||
|
const res = await api.get<PaginatedData<PatientInfo>>('/health/patients');
|
||||||
|
return Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ export interface TodaySummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getTodaySummary(patientId?: string) {
|
export async function getTodaySummary(patientId?: string) {
|
||||||
|
const pid = patientId || Taro.getStorageSync('current_patient_id') || '';
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (patientId) params.patient_id = patientId;
|
if (pid) params.patient_id = pid;
|
||||||
return api.get<TodaySummary>('/health/vital-signs/today', params);
|
return api.get<TodaySummary>('/health/vital-signs/today', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ConcurrencyLimiter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const limiter = new ConcurrencyLimiter(12);
|
const limiter = new ConcurrencyLimiter(8);
|
||||||
|
|
||||||
// --- Response cache + deduplication ---
|
// --- Response cache + deduplication ---
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
setCachedPatientId(currentPatient.id);
|
setCachedPatientId(currentPatient.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态有变化时清理请求缓存,避免返回过期数据
|
||||||
|
clearRequestCache();
|
||||||
|
|
||||||
// 跳过未变更的 set()
|
// 跳过未变更的 set()
|
||||||
const cur = get();
|
const cur = get();
|
||||||
const userChanged = cur.user?.id !== user?.id;
|
const userChanged = cur.user?.id !== user?.id;
|
||||||
@@ -145,6 +148,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
secureSet('tenant_id', user.tenant_id || '');
|
secureSet('tenant_id', user.tenant_id || '');
|
||||||
set({ user, roles, loading: false });
|
set({ user, roles, loading: false });
|
||||||
clearLoggingOut();
|
clearLoggingOut();
|
||||||
|
get().loadPatients();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
secureSet('wechat_openid', resp.openid);
|
secureSet('wechat_openid', resp.openid);
|
||||||
@@ -176,6 +180,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
secureSet('tenant_id', resp.user?.tenant_id || tenantId);
|
secureSet('tenant_id', resp.user?.tenant_id || tenantId);
|
||||||
set({ user: resp.user, roles, loading: false });
|
set({ user: resp.user, roles, loading: false });
|
||||||
clearLoggingOut();
|
clearLoggingOut();
|
||||||
|
// 登录成功后自动加载患者档案(如果有的话)
|
||||||
|
get().loadPatients();
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
@@ -209,6 +215,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
secureRemove('wechat_openid');
|
secureRemove('wechat_openid');
|
||||||
set({ user: tokenData.user, roles, loading: false });
|
set({ user: tokenData.user, roles, loading: false });
|
||||||
clearLoggingOut();
|
clearLoggingOut();
|
||||||
|
get().loadPatients();
|
||||||
return true;
|
return true;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
secureRemove('wechat_openid');
|
secureRemove('wechat_openid');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import * as healthApi from '@/services/health';
|
import * as healthApi from '@/services/health';
|
||||||
import { getCachedPatientId } from '@/services/request';
|
import { getCachedPatientId } from '@/services/request';
|
||||||
|
import { useAuthStore } from './auth';
|
||||||
|
|
||||||
interface CachedTrend {
|
interface CachedTrend {
|
||||||
data: { date: string; value: number }[];
|
data: { date: string; value: number }[];
|
||||||
@@ -37,7 +38,9 @@ export const useHealthStore = create<HealthState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
set({ _refreshingToday: true, loading: true });
|
set({ _refreshingToday: true, loading: true });
|
||||||
try {
|
try {
|
||||||
const patientId = getCachedPatientId() || undefined;
|
const patientId = getCachedPatientId()
|
||||||
|
|| useAuthStore.getState().currentPatient?.id
|
||||||
|
|| undefined;
|
||||||
const data = await healthApi.getTodaySummary(patientId);
|
const data = await healthApi.getTodaySummary(patientId);
|
||||||
set({ todaySummary: data, todaySummaryFetchedAt: Date.now(), loading: false, _refreshingToday: false });
|
set({ todaySummary: data, todaySummaryFetchedAt: Date.now(), loading: false, _refreshingToday: false });
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user