fix(mp): 患者端卡死深度审查修复 — CRITICAL 回归 + 并发保护 + 页栈溢出防护
CRITICAL: - 咨询详情页 loadData 引用已删除的 pollingRef → 移除残余引用 HIGH: - 401 重试递归改循环结构,避免并发限制器双 slot 占用 - 医生端 4 个列表页添加 loadingRef 防重入(consultation/alerts/dialysis/prescription) - 新增 safeNavigateTo 页栈溢出保护(栈≥9 自动 redirectTo) 前期修复一并提交: - 全局并发限制 MAX_CONCURRENT=8 - doRefresh 失败时完整清理 Storage + 重置缓存状态 - 401 跳转登录页修正 - 长轮询 generation counter 模式 - 首页/健康页 loadingRef + refreshToday 去重
This commit is contained in:
@@ -32,7 +32,7 @@ export default function ConsultationDetail() {
|
||||
const [sending, setSending] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef('');
|
||||
const pollingRef = useRef(false);
|
||||
const pollingGeneration = useRef(0);
|
||||
const mountedRef = useRef(true);
|
||||
const messagesRef = useRef<ConsultationMessage[]>([]);
|
||||
const modeClass = useElderClass();
|
||||
@@ -44,40 +44,40 @@ export default function ConsultationDetail() {
|
||||
startLongPolling();
|
||||
}
|
||||
return () => {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
// 页面可见时恢复轮询,不可见时暂停(防止 DevTools 后台页面累积轮询)
|
||||
useDidShow(() => {
|
||||
if (sessionId && !pollingRef.current && session?.status !== 'closed') {
|
||||
if (sessionId && session?.status !== 'closed') {
|
||||
startLongPolling();
|
||||
}
|
||||
});
|
||||
useDidHide(() => {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.status === 'closed') {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
}
|
||||
}, [session?.status]);
|
||||
|
||||
const startLongPolling = () => {
|
||||
pollingRef.current = true;
|
||||
longPoll();
|
||||
const gen = ++pollingGeneration.current;
|
||||
longPoll(gen);
|
||||
};
|
||||
|
||||
const longPoll = async (failCount = 0) => {
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
const longPoll = async (gen: number, failCount = 0) => {
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
if (failCount >= MAX_CONSECUTIVE_FAILURES) return;
|
||||
try {
|
||||
const currentMessages = messagesRef.current;
|
||||
const lastId = currentMessages.length > 0 ? currentMessages[currentMessages.length - 1].id : undefined;
|
||||
const newMsgs = await pollMessages(sessionId, lastId);
|
||||
if (!mountedRef.current) return;
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
if (newMsgs && newMsgs.length > 0) {
|
||||
setMessages((prev) => {
|
||||
const existing = new Set(prev.map((msg) => msg.id));
|
||||
@@ -92,11 +92,11 @@ export default function ConsultationDetail() {
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
const delay = failCount > 0 ? Math.min(failCount * 2000, 30000) : POLL_INTERVAL_MS;
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
if (pollingRef.current && mountedRef.current) {
|
||||
longPoll(failCount);
|
||||
if (gen === pollingGeneration.current && mountedRef.current) {
|
||||
longPoll(gen, failCount);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,7 +112,6 @@ export default function ConsultationDetail() {
|
||||
setMessages(msgs);
|
||||
messagesRef.current = msgs;
|
||||
scrollViewRef.current = `msg-${msgs.length}`;
|
||||
if (s.status === 'closed') pollingRef.current = false;
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { listAlerts, type Alert } from '@/services/doctor/alerts';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import './index.scss';
|
||||
|
||||
const SEVERITY_MAP: Record<string, { label: string; className: string }> = {
|
||||
@@ -35,6 +36,7 @@ export default function AlertList() {
|
||||
const [activeTab, setActiveTab] = useState('');
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
|
||||
|
||||
@@ -43,6 +45,8 @@ export default function AlertList() {
|
||||
}, [page, activeTab]);
|
||||
|
||||
const loadAlerts = async () => {
|
||||
if (loadingRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listAlerts({
|
||||
@@ -56,6 +60,7 @@ export default function AlertList() {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +70,7 @@ export default function AlertList() {
|
||||
};
|
||||
|
||||
const handleAlertClick = (alert: Alert) => {
|
||||
Taro.navigateTo({ url: `/pages/doctor/alerts/detail/index?id=${alert.id}` });
|
||||
safeNavigateTo(`/pages/doctor/alerts/detail/index?id=${alert.id}`);
|
||||
};
|
||||
|
||||
const formatTime = (dateStr: string) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function ConsultationDetail() {
|
||||
const [sending, setSending] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef('');
|
||||
const pollingRef = useRef(false);
|
||||
const pollingGeneration = useRef(0);
|
||||
const mountedRef = useRef(true);
|
||||
const messagesRef = useRef<ConsultationMessage[]>([]);
|
||||
|
||||
@@ -40,40 +40,40 @@ export default function ConsultationDetail() {
|
||||
startLongPolling();
|
||||
}
|
||||
return () => {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, [sessionId]);
|
||||
|
||||
// 页面可见时恢复轮询,不可见时暂停(防止 DevTools 后台页面累积轮询)
|
||||
useDidShow(() => {
|
||||
if (sessionId && !pollingRef.current && session?.status !== 'closed') {
|
||||
if (sessionId && session?.status !== 'closed') {
|
||||
startLongPolling();
|
||||
}
|
||||
});
|
||||
useDidHide(() => {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.status === 'closed') {
|
||||
pollingRef.current = false;
|
||||
pollingGeneration.current += 1;
|
||||
}
|
||||
}, [session?.status]);
|
||||
|
||||
const startLongPolling = () => {
|
||||
pollingRef.current = true;
|
||||
longPoll();
|
||||
const gen = ++pollingGeneration.current;
|
||||
longPoll(gen);
|
||||
};
|
||||
|
||||
const longPoll = async (failCount = 0) => {
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
const longPoll = async (gen: number, failCount = 0) => {
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
if (failCount >= MAX_CONSECUTIVE_FAILURES) return;
|
||||
try {
|
||||
const currentMessages = messagesRef.current;
|
||||
const lastId = currentMessages.length > 0 ? currentMessages[currentMessages.length - 1].id : undefined;
|
||||
const newMsgs = await pollMessages(sessionId, lastId);
|
||||
if (!mountedRef.current) return;
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
if (newMsgs && newMsgs.length > 0) {
|
||||
setMessages((prev) => {
|
||||
const existing = new Set(prev.map((msg) => msg.id));
|
||||
@@ -88,11 +88,11 @@ export default function ConsultationDetail() {
|
||||
} catch {
|
||||
failCount++;
|
||||
}
|
||||
if (!pollingRef.current || !mountedRef.current) return;
|
||||
if (gen !== pollingGeneration.current || !mountedRef.current) return;
|
||||
const delay = failCount > 0 ? Math.min(failCount * 2000, 30000) : POLL_INTERVAL_MS;
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
if (pollingRef.current && mountedRef.current) {
|
||||
longPoll(failCount);
|
||||
if (gen === pollingGeneration.current && mountedRef.current) {
|
||||
longPoll(gen, failCount);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,7 +108,6 @@ export default function ConsultationDetail() {
|
||||
setMessages(msgs);
|
||||
messagesRef.current = msgs;
|
||||
scrollViewRef.current = `msg-${msgs.length}`;
|
||||
if (s.status === 'closed') pollingRef.current = false;
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { listSessions, type ConsultationSession } from '@/services/doctor/consultation';
|
||||
@@ -7,6 +7,7 @@ import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { getStatusInlineStyle, getStatusLabel } from '@/utils/statusTag';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import './index.scss';
|
||||
|
||||
const TABS = [
|
||||
@@ -23,6 +24,7 @@ export default function ConsultationList() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
const totalPages = useMemo(() => Math.ceil(total / 20), [total]);
|
||||
|
||||
@@ -31,6 +33,8 @@ export default function ConsultationList() {
|
||||
}, [page, activeTab]);
|
||||
|
||||
const loadSessions = async () => {
|
||||
if (loadingRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listSessions({
|
||||
@@ -44,6 +48,7 @@ export default function ConsultationList() {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,7 +87,7 @@ export default function ConsultationList() {
|
||||
<View
|
||||
key={s.id}
|
||||
className='session-card'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/doctor/consultation/detail/index?id=${s.id}` })}
|
||||
onClick={() => safeNavigateTo(`/pages/doctor/consultation/detail/index?id=${s.id}`)}
|
||||
>
|
||||
<View className='session-card__top'>
|
||||
<Text className='session-card__subject'>{s.subject || '在线咨询'}</Text>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { listDialysisRecords, type DialysisRecord } from '@/services/doctor/dialysis';
|
||||
@@ -6,6 +6,7 @@ import { listPatients } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import './index.scss';
|
||||
|
||||
const TABS = [
|
||||
@@ -28,12 +29,15 @@ export default function DialysisList() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPatientId) loadRecords(1);
|
||||
}, [currentPatientId, activeTab]);
|
||||
|
||||
const loadRecords = async (p: number) => {
|
||||
if (loadingRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: { page: number; page_size: number; status?: string } = { page: p, page_size: 20 };
|
||||
@@ -46,6 +50,7 @@ export default function DialysisList() {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -114,9 +119,7 @@ export default function DialysisList() {
|
||||
<View
|
||||
key={r.id}
|
||||
className='record-card'
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/pages/doctor/dialysis/detail/index?id=${r.id}`,
|
||||
})}
|
||||
onClick={() => safeNavigateTo(`/pages/doctor/dialysis/detail/index?id=${r.id}`)}
|
||||
>
|
||||
<View className='record-card__header'>
|
||||
<Text className={`type-tag type-tag--${(r.dialysis_type || 'hd').toLowerCase()}`}>
|
||||
@@ -164,7 +167,7 @@ export default function DialysisList() {
|
||||
Taro.showToast({ title: '请先选择患者', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
Taro.navigateTo({ url: `/pages/doctor/dialysis/create/index?patientId=${currentPatientId}` });
|
||||
safeNavigateTo(`/pages/doctor/dialysis/create/index?patientId=${currentPatientId}`);
|
||||
}}
|
||||
>
|
||||
<Text className='fab-text'>+</Text>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { listDialysisPrescriptions, type DialysisPrescription } from '@/services/doctor/dialysis';
|
||||
@@ -6,6 +6,7 @@ import { listPatients } from '@/services/doctor/patient';
|
||||
import Loading from '@/components/Loading';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { safeNavigateTo } from '@/utils/navigate';
|
||||
import './index.scss';
|
||||
|
||||
const TABS = [
|
||||
@@ -25,12 +26,15 @@ export default function PrescriptionList() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData(1);
|
||||
}, [currentPatientId, activeTab]);
|
||||
|
||||
const loadData = async (p: number) => {
|
||||
if (loadingRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listDialysisPrescriptions({
|
||||
@@ -46,6 +50,7 @@ export default function PrescriptionList() {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
loadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,9 +109,7 @@ export default function PrescriptionList() {
|
||||
<View
|
||||
key={p.id}
|
||||
className='prescription-card'
|
||||
onClick={() => Taro.navigateTo({
|
||||
url: `/pages/doctor/prescription/detail/index?id=${p.id}`,
|
||||
})}
|
||||
onClick={() => safeNavigateTo(`/pages/doctor/prescription/detail/index?id=${p.id}`)}
|
||||
>
|
||||
<View className='prescription-card__header'>
|
||||
<Text className='prescription-card__model'>{p.dialyzer_model || '透析处方'}</Text>
|
||||
@@ -156,7 +159,7 @@ export default function PrescriptionList() {
|
||||
Taro.showToast({ title: '请先选择患者', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
Taro.navigateTo({ url: `/pages/doctor/prescription/create/index?patientId=${currentPatientId}` });
|
||||
safeNavigateTo(`/pages/doctor/prescription/create/index?patientId=${currentPatientId}`);
|
||||
}}
|
||||
>
|
||||
<Text className='fab-text'>+</Text>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input } from '@tarojs/components';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
@@ -61,16 +61,18 @@ export default function Health() {
|
||||
const [trendLoading, setTrendLoading] = useState(false);
|
||||
const [aiSuggestions, setAiSuggestions] = useState<AiSuggestionItem[]>([]);
|
||||
const [thresholds, setThresholds] = useState<HealthThreshold[]>(DEFAULT_THRESHOLDS);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
if (!user) return;
|
||||
if (!user || loadingRef.current) return;
|
||||
// 批量发起请求,避免串行 setState 级联重渲染
|
||||
loadingRef.current = true;
|
||||
Promise.allSettled([
|
||||
refreshToday(),
|
||||
loadTrend(activeTab),
|
||||
loadAiSuggestions(),
|
||||
getHealthThresholds().then((t) => { if (t.length > 0) setThresholds(t); }),
|
||||
]);
|
||||
]).finally(() => { loadingRef.current = false; });
|
||||
}, 5000);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useRef } from 'react';
|
||||
import Taro, { usePullDownRefresh, useDidShow, useDidHide } from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useUIStore } from '../../stores/ui';
|
||||
@@ -190,6 +190,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
const [reminders, setReminders] = useState<ReminderItem[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [remindersLoading, setRemindersLoading] = useState(false);
|
||||
const remindersLoadingRef = useRef(false);
|
||||
|
||||
const { trigger: triggerHomeRefresh } = useThrottledDidShow(() => {
|
||||
refreshToday();
|
||||
@@ -216,7 +217,8 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
|
||||
const loadReminders = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId) return;
|
||||
if (!patientId || remindersLoadingRef.current) return;
|
||||
remindersLoadingRef.current = true;
|
||||
setRemindersLoading(true);
|
||||
try {
|
||||
const items: ReminderItem[] = [];
|
||||
@@ -257,6 +259,7 @@ function HomeDashboard({ modeClass }: { modeClass: string }) {
|
||||
} catch {
|
||||
setReminders([]);
|
||||
} finally {
|
||||
remindersLoadingRef.current = false;
|
||||
setRemindersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,65 +103,102 @@ async function doRefresh(): Promise<boolean> {
|
||||
} catch {
|
||||
// token 刷新失败
|
||||
}
|
||||
isLoggingOut = true;
|
||||
secureRemove('access_token');
|
||||
secureRemove('refresh_token');
|
||||
secureRemove('user_data');
|
||||
secureRemove('user_roles');
|
||||
secureRemove('tenant_id');
|
||||
secureRemove('wechat_openid');
|
||||
Taro.removeStorageSync('current_patient');
|
||||
Taro.removeStorageSync('current_patient_id');
|
||||
clearRequestCache();
|
||||
cachedPatientId = '';
|
||||
headersCacheTs = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Core request ---
|
||||
async function request<T>(method: string, path: string, data?: unknown, timeout?: number, _retryCount401 = 0): Promise<T> {
|
||||
const headers = await getHeaders();
|
||||
const url = `${BASE_URL}${path}`;
|
||||
let res: Taro.request.SuccessCallbackResult;
|
||||
try {
|
||||
res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 });
|
||||
} catch (err: any) {
|
||||
const msg = err?.errMsg || '';
|
||||
if (msg.includes('timeout')) {
|
||||
Taro.showToast({ title: '网络超时,请重试', icon: 'none' });
|
||||
throw new Error('网络超时');
|
||||
}
|
||||
Taro.showToast({ title: '网络异常,请检查连接', icon: 'none' });
|
||||
throw new Error('网络异常');
|
||||
}
|
||||
// 微信小程序并发请求限制为 10 个,超出会排队阻塞
|
||||
const MAX_CONCURRENT = 8;
|
||||
let activeRequests = 0;
|
||||
const pendingQueue: Array<() => void> = [];
|
||||
|
||||
if (res.statusCode === 401) {
|
||||
if (isLoggingOut || _retryCount401 >= MAX_401_RETRY) {
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
const hasToken = !!safeGet('access_token');
|
||||
if (hasToken) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) return request<T>(method, path, data, timeout, _retryCount401 + 1);
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPath = pages[pages.length - 1]?.path || '';
|
||||
if (!currentPath.includes('pages/login')) {
|
||||
Taro.reLaunch({ url: '/pages/index/index' });
|
||||
function acquireSlot(): Promise<void> {
|
||||
if (activeRequests < MAX_CONCURRENT) {
|
||||
activeRequests++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => { pendingQueue.push(resolve); });
|
||||
}
|
||||
|
||||
function releaseSlot(): void {
|
||||
activeRequests--;
|
||||
const next = pendingQueue.shift();
|
||||
if (next) { activeRequests++; next(); }
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, data?: unknown, timeout?: number): Promise<T> {
|
||||
let retryCount401 = 0;
|
||||
for (;;) {
|
||||
await acquireSlot();
|
||||
try {
|
||||
const headers = await getHeaders();
|
||||
const url = `${BASE_URL}${path}`;
|
||||
let res: Taro.request.SuccessCallbackResult;
|
||||
try {
|
||||
res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 });
|
||||
} catch (err: any) {
|
||||
const msg = err?.errMsg || '';
|
||||
if (msg.includes('timeout')) {
|
||||
Taro.showToast({ title: '网络超时,请重试', icon: 'none' });
|
||||
throw new Error('网络超时');
|
||||
}
|
||||
Taro.showToast({ title: '网络异常,请检查连接', icon: 'none' });
|
||||
throw new Error('网络异常');
|
||||
}
|
||||
|
||||
if (res.statusCode === 401) {
|
||||
if (isLoggingOut || retryCount401 >= MAX_401_RETRY) {
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
const hasToken = !!safeGet('access_token');
|
||||
if (hasToken) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) {
|
||||
isLoggingOut = false;
|
||||
retryCount401++;
|
||||
continue;
|
||||
}
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPath = pages[pages.length - 1]?.path || '';
|
||||
if (!currentPath.includes('pages/login')) {
|
||||
Taro.reLaunch({ url: '/pages/login/index' });
|
||||
}
|
||||
}
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
|
||||
if (res.statusCode === 403) {
|
||||
Taro.showToast({ title: '权限不足', icon: 'none' });
|
||||
throw new Error('权限不足');
|
||||
}
|
||||
|
||||
if (res.statusCode >= 500) {
|
||||
Taro.showToast({ title: '服务器繁忙,请稍后重试', icon: 'none' });
|
||||
throw new Error('服务器错误');
|
||||
}
|
||||
|
||||
const body = res.data as ApiResponse<T>;
|
||||
if (!body.success) {
|
||||
const userMsg = body.error_code ? (ERROR_CODE_MAP[body.error_code] || '操作失败,请稍后重试') : '操作失败,请稍后重试';
|
||||
throw new Error(userMsg);
|
||||
}
|
||||
return body.data as T;
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
|
||||
if (res.statusCode === 403) {
|
||||
Taro.showToast({ title: '权限不足', icon: 'none' });
|
||||
throw new Error('权限不足');
|
||||
}
|
||||
|
||||
if (res.statusCode >= 500) {
|
||||
Taro.showToast({ title: '服务器繁忙,请稍后重试', icon: 'none' });
|
||||
throw new Error('服务器错误');
|
||||
}
|
||||
|
||||
const body = res.data as ApiResponse<T>;
|
||||
if (!body.success) {
|
||||
const userMsg = body.error_code ? (ERROR_CODE_MAP[body.error_code] || '操作失败,请稍后重试') : '操作失败,请稍后重试';
|
||||
throw new Error(userMsg);
|
||||
}
|
||||
return body.data as T;
|
||||
}
|
||||
|
||||
function buildQuery(params?: Record<string, string | number | undefined>): string {
|
||||
|
||||
@@ -21,6 +21,8 @@ const CACHE_TTL = 5 * 60 * 1000;
|
||||
const TODAY_SUMMARY_TTL = 60_000;
|
||||
const MAX_TREND_KEYS = 20;
|
||||
|
||||
let refreshingToday = false;
|
||||
|
||||
export const useHealthStore = create<HealthState>((set, get) => ({
|
||||
todaySummary: null,
|
||||
todaySummaryFetchedAt: 0,
|
||||
@@ -28,10 +30,12 @@ export const useHealthStore = create<HealthState>((set, get) => ({
|
||||
loading: false,
|
||||
|
||||
refreshToday: async (force = false) => {
|
||||
if (refreshingToday) return;
|
||||
const state = get();
|
||||
if (!force && state.todaySummary && Date.now() - state.todaySummaryFetchedAt < TODAY_SUMMARY_TTL) {
|
||||
return;
|
||||
}
|
||||
refreshingToday = true;
|
||||
set({ loading: true });
|
||||
try {
|
||||
const patientId = Taro.getStorageSync('current_patient_id') || undefined;
|
||||
@@ -39,6 +43,8 @@ export const useHealthStore = create<HealthState>((set, get) => ({
|
||||
set({ todaySummary: data, todaySummaryFetchedAt: Date.now(), loading: false });
|
||||
} catch {
|
||||
set({ loading: false });
|
||||
} finally {
|
||||
refreshingToday = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
const LOGIN_PAGE = '/pages/login/index';
|
||||
const MAX_PAGE_STACK = 9;
|
||||
|
||||
export function navigateToLogin() {
|
||||
Taro.reLaunch({ url: LOGIN_PAGE });
|
||||
}
|
||||
|
||||
export function safeNavigateTo(url: string): void {
|
||||
const pages = Taro.getCurrentPages();
|
||||
if (pages.length >= MAX_PAGE_STACK) {
|
||||
Taro.redirectTo({ url });
|
||||
} else {
|
||||
Taro.navigateTo({ url });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user