fix(mp): setTimeout 无清理修复 — useSafeTimeout hook + 10 页面接入
新增 useSafeTimeout hook,页面隐藏时自动清理所有定时器。 10 个页面接入:daily-monitoring、exchange、family-add、 health/input、prescription detail/create、dialysis detail/create、 appointment detail/create。所有 fire-and-forget setTimeout 替换为 safeSetTimeout,避免页面切走后定时器回调在错误上下文执行。
This commit is contained in:
23
apps/miniprogram/src/hooks/useSafeTimeout.ts
Normal file
23
apps/miniprogram/src/hooks/useSafeTimeout.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { useDidHide } from '@tarojs/taro';
|
||||
|
||||
export function useSafeTimeout() {
|
||||
const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
|
||||
const safeSetTimeout = useCallback((fn: () => void, ms: number) => {
|
||||
const id = setTimeout(() => {
|
||||
timers.current = timers.current.filter((t) => t !== id);
|
||||
fn();
|
||||
}, ms);
|
||||
timers.current.push(id);
|
||||
}, []);
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
timers.current.forEach(clearTimeout);
|
||||
timers.current = [];
|
||||
}, []);
|
||||
|
||||
useDidHide(() => clearAll());
|
||||
|
||||
return { safeSetTimeout, clearAll };
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { trackEvent } from '@/services/analytics';
|
||||
import StepIndicator from '../../../components/StepIndicator';
|
||||
import WeekCalendar from '../../../components/WeekCalendar';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
const DEPARTMENTS = [
|
||||
@@ -43,6 +44,7 @@ export default function AppointmentCreate() {
|
||||
const [timeSlot, setTimeSlot] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
const [schedules, setSchedules] = useState<any[]>([]);
|
||||
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
||||
const modeClass = useElderClass();
|
||||
@@ -127,7 +129,7 @@ export default function AppointmentCreate() {
|
||||
await (Taro.requestSubscribeMessage as any)({ tmplIds: [tmplId] });
|
||||
} catch { /* 用户拒绝 */ }
|
||||
}
|
||||
setTimeout(() => Taro.navigateBack(), 1500);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1500);
|
||||
} catch {
|
||||
Taro.showToast({ title: '预约失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Appointment } from '../../../services/appointment';
|
||||
import Loading from '../../../components/Loading';
|
||||
import ErrorState from '../../../components/ErrorState';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; className: string }> = {
|
||||
@@ -23,6 +24,7 @@ export default function AppointmentDetail() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const [cancelling, setCancelling] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
const modeClass = useElderClass();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,7 +57,7 @@ export default function AppointmentDetail() {
|
||||
try {
|
||||
await cancelAppointment(appointment.id, appointment.version);
|
||||
Taro.showToast({ title: '已取消预约', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1500);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1500);
|
||||
} catch {
|
||||
Taro.showToast({ title: '取消失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
const DIALYSIS_TYPES = ['HD', 'HDF', 'HF'];
|
||||
@@ -63,6 +64,7 @@ export default function DialysisCreate() {
|
||||
const [form, setForm] = useState<FormState>({ ...initialForm, patient_id: patientIdFromRoute });
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && id) loadRecord();
|
||||
@@ -145,7 +147,7 @@ export default function DialysisCreate() {
|
||||
await createDialysisRecord(payload);
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' });
|
||||
}
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.showToast({ title: isEdit ? '更新失败' : '创建失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
export default function DialysisDetail() {
|
||||
@@ -17,6 +18,7 @@ export default function DialysisDetail() {
|
||||
const [record, setRecord] = useState<DialysisRecord | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) loadRecord();
|
||||
@@ -73,7 +75,7 @@ export default function DialysisDetail() {
|
||||
try {
|
||||
await deleteDialysisRecord(id, record.version);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' });
|
||||
setSubmitting(false);
|
||||
|
||||
@@ -4,6 +4,7 @@ import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { createDialysisPrescription } from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
interface FormState {
|
||||
@@ -54,6 +55,7 @@ export default function PrescriptionCreate() {
|
||||
const modeClass = useElderClass();
|
||||
const [form, setForm] = useState<FormState>(initialForm);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
const updateField = (key: keyof FormState, value: string) => {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
@@ -92,7 +94,7 @@ export default function PrescriptionCreate() {
|
||||
try {
|
||||
await createDialysisPrescription(payload);
|
||||
Taro.showToast({ title: '创建成功', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.showToast({ title: '创建失败', icon: 'none' });
|
||||
} finally {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@/services/doctor/dialysis';
|
||||
import Loading from '@/components/Loading';
|
||||
import { useElderClass } from '../../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
export default function PrescriptionDetail() {
|
||||
@@ -16,6 +17,7 @@ export default function PrescriptionDetail() {
|
||||
const [rx, setRx] = useState<DialysisPrescription | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) loadRx();
|
||||
@@ -63,7 +65,7 @@ export default function PrescriptionDetail() {
|
||||
try {
|
||||
await deleteDialysisPrescription(id, rx.version);
|
||||
Taro.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.showToast({ title: '删除失败', icon: 'none' });
|
||||
setSubmitting(false);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useHealthStore } from '@/stores/health';
|
||||
import { usePointsStore } from '@/stores/points';
|
||||
import { clearRequestCache } from '@/services/request';
|
||||
import { trackEvent } from '@/services/analytics';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import './index.scss';
|
||||
|
||||
@@ -85,6 +86,7 @@ export default function DailyMonitoring() {
|
||||
const [urineOutput, setUrineOutput] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
// ── Collapsible sections ──
|
||||
const [collapsed, setCollapsed] = useState<Record<SectionKey, boolean>>({
|
||||
@@ -225,11 +227,11 @@ export default function DailyMonitoring() {
|
||||
usePointsStore.getState().invalidate();
|
||||
Taro.showToast({ title: '上报成功', icon: 'success' });
|
||||
|
||||
setTimeout(() => {
|
||||
safeSetTimeout(() => {
|
||||
Taro.showToast({ title: '+10 健康积分', icon: 'none', duration: 1500 });
|
||||
}, 1600);
|
||||
|
||||
setTimeout(() => {
|
||||
safeSetTimeout(() => {
|
||||
Taro.navigateBack();
|
||||
}, 3200);
|
||||
} catch (e: unknown) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAuthStore } from '../../../stores/auth';
|
||||
import { useHealthStore } from '@/stores/health';
|
||||
import { usePointsStore } from '@/stores/points';
|
||||
import { clearRequestCache } from '@/services/request';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import { trackEvent } from '@/services/analytics';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import Loading from '../../../components/Loading';
|
||||
@@ -61,6 +62,7 @@ export default function HealthInput() {
|
||||
const [diastolic, setDiastolic] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
const [loadingThresholds, setLoadingThresholds] = useState(true);
|
||||
const currentPatient = useAuthStore((s) => s.currentPatient);
|
||||
const clearCache = useHealthStore((s) => s.clearCache);
|
||||
@@ -155,7 +157,7 @@ export default function HealthInput() {
|
||||
usePointsStore.getState().invalidate();
|
||||
Taro.showToast({ title: '录入成功', icon: 'success' });
|
||||
trackEvent('health_data_input', { type: currentIndicator });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : '录入失败';
|
||||
Taro.showToast({ title: msg, icon: 'none' });
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { PointsProduct } from '../../../services/points';
|
||||
import { usePointsStore } from '../../../stores/points';
|
||||
import Loading from '../../../components/Loading';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
const TYPE_INITIAL: Record<string, string> = {
|
||||
@@ -37,6 +38,7 @@ export default function ExchangeConfirm() {
|
||||
const refreshPoints = usePointsStore((s) => s.refresh);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useThrottledDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '确认兑换' });
|
||||
@@ -48,7 +50,7 @@ export default function ExchangeConfirm() {
|
||||
const productId = instance.router?.params?.product_id;
|
||||
if (!productId) {
|
||||
Taro.showToast({ title: '参数错误', icon: 'none' });
|
||||
setTimeout(() => Taro.navigateBack(), 1500);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,13 +63,13 @@ export default function ExchangeConfirm() {
|
||||
const found = productRes.data.find((p) => p.id === productId);
|
||||
if (!found) {
|
||||
Taro.showToast({ title: '商品不存在', icon: 'none' });
|
||||
setTimeout(() => Taro.navigateBack(), 1500);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1500);
|
||||
return;
|
||||
}
|
||||
setProduct(found);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
setTimeout(() => Taro.navigateBack(), 1500);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1500);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -96,7 +98,7 @@ export default function ExchangeConfirm() {
|
||||
const order = await exchangeProduct(product.id);
|
||||
Taro.showToast({ title: '兑换成功', icon: 'success', duration: 2000 });
|
||||
|
||||
setTimeout(() => {
|
||||
safeSetTimeout(() => {
|
||||
Taro.showModal({
|
||||
title: '兑换成功',
|
||||
content: `核销码: ${order.qr_code}\n请凭此码到前台核销`,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { createPatient, updatePatient, Patient } from '../../../services/patient';
|
||||
import { useElderClass } from '../../../hooks/useElderClass';
|
||||
import { useSafeTimeout } from '@/hooks/useSafeTimeout';
|
||||
import './index.scss';
|
||||
|
||||
const RELATION_OPTIONS = ['本人', '配偶', '父母', '子女', '其他'];
|
||||
@@ -23,6 +24,7 @@ export default function FamilyAdd() {
|
||||
);
|
||||
const [birthDate, setBirthDate] = useState(editData?.birth_date || '');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { safeSetTimeout } = useSafeTimeout();
|
||||
|
||||
useEffect(() => {
|
||||
return () => { Taro.removeStorageSync('edit_patient'); };
|
||||
@@ -54,7 +56,7 @@ export default function FamilyAdd() {
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: '添加成功', icon: 'success' });
|
||||
}
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
safeSetTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch {
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({ title: editId ? '修改失败' : '添加失败', icon: 'none' });
|
||||
|
||||
Reference in New Issue
Block a user