From fed175998548db67d183af8b4f2ca8ea26a69293 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 15 May 2026 00:38:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(mp):=20setTimeout=20=E6=97=A0=E6=B8=85?= =?UTF-8?q?=E7=90=86=E4=BF=AE=E5=A4=8D=20=E2=80=94=20useSafeTimeout=20hook?= =?UTF-8?q?=20+=2010=20=E9=A1=B5=E9=9D=A2=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 useSafeTimeout hook,页面隐藏时自动清理所有定时器。 10 个页面接入:daily-monitoring、exchange、family-add、 health/input、prescription detail/create、dialysis detail/create、 appointment detail/create。所有 fire-and-forget setTimeout 替换为 safeSetTimeout,避免页面切走后定时器回调在错误上下文执行。 --- apps/miniprogram/src/hooks/useSafeTimeout.ts | 23 +++++++++++++++++++ .../src/pages/appointment/create/index.tsx | 4 +++- .../src/pages/appointment/detail/index.tsx | 4 +++- .../pages/doctor/dialysis/create/index.tsx | 4 +++- .../pages/doctor/dialysis/detail/index.tsx | 4 +++- .../doctor/prescription/create/index.tsx | 4 +++- .../doctor/prescription/detail/index.tsx | 4 +++- .../pkg-health/daily-monitoring/index.tsx | 6 +++-- .../src/pages/pkg-health/input/index.tsx | 4 +++- .../src/pages/pkg-mall/exchange/index.tsx | 10 ++++---- .../pages/pkg-profile/family-add/index.tsx | 4 +++- 11 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 apps/miniprogram/src/hooks/useSafeTimeout.ts diff --git a/apps/miniprogram/src/hooks/useSafeTimeout.ts b/apps/miniprogram/src/hooks/useSafeTimeout.ts new file mode 100644 index 0000000..f70b76c --- /dev/null +++ b/apps/miniprogram/src/hooks/useSafeTimeout.ts @@ -0,0 +1,23 @@ +import { useRef, useCallback } from 'react'; +import { useDidHide } from '@tarojs/taro'; + +export function useSafeTimeout() { + const timers = useRef[]>([]); + + 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 }; +} diff --git a/apps/miniprogram/src/pages/appointment/create/index.tsx b/apps/miniprogram/src/pages/appointment/create/index.tsx index ac076f9..f2df209 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.tsx +++ b/apps/miniprogram/src/pages/appointment/create/index.tsx @@ -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([]); const [timeSlots, setTimeSlots] = useState([]); 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 { diff --git a/apps/miniprogram/src/pages/appointment/detail/index.tsx b/apps/miniprogram/src/pages/appointment/detail/index.tsx index 8440955..8bd774b 100644 --- a/apps/miniprogram/src/pages/appointment/detail/index.tsx +++ b/apps/miniprogram/src/pages/appointment/detail/index.tsx @@ -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 = { @@ -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 { diff --git a/apps/miniprogram/src/pages/doctor/dialysis/create/index.tsx b/apps/miniprogram/src/pages/doctor/dialysis/create/index.tsx index d66f4ed..621c943 100644 --- a/apps/miniprogram/src/pages/doctor/dialysis/create/index.tsx +++ b/apps/miniprogram/src/pages/doctor/dialysis/create/index.tsx @@ -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({ ...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 { diff --git a/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx b/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx index 8144a52..18c84ad 100644 --- a/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/dialysis/detail/index.tsx @@ -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(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); diff --git a/apps/miniprogram/src/pages/doctor/prescription/create/index.tsx b/apps/miniprogram/src/pages/doctor/prescription/create/index.tsx index 2a57adf..b660f0b 100644 --- a/apps/miniprogram/src/pages/doctor/prescription/create/index.tsx +++ b/apps/miniprogram/src/pages/doctor/prescription/create/index.tsx @@ -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(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 { diff --git a/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx b/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx index 955f738..c25bd2a 100644 --- a/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx +++ b/apps/miniprogram/src/pages/doctor/prescription/detail/index.tsx @@ -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(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); diff --git a/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx b/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx index cb6d1e9..251cdb6 100644 --- a/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/daily-monitoring/index.tsx @@ -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>({ @@ -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) { diff --git a/apps/miniprogram/src/pages/pkg-health/input/index.tsx b/apps/miniprogram/src/pages/pkg-health/input/index.tsx index ac0ee57..85e9cfa 100644 --- a/apps/miniprogram/src/pages/pkg-health/input/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/input/index.tsx @@ -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' }); diff --git a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx index 54ac68b..7c1c61b 100644 --- a/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx +++ b/apps/miniprogram/src/pages/pkg-mall/exchange/index.tsx @@ -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 = { @@ -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请凭此码到前台核销`, diff --git a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx index 0499826..1ff5caf 100644 --- a/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx +++ b/apps/miniprogram/src/pages/pkg-profile/family-add/index.tsx @@ -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' });