Files
hms/apps/miniprogram/src/pages/pkg-health/daily-monitoring/useDailyMonitoring.ts
iven 4fcbf705ca refactor(mp): E3-2 大文件拆分 + U3-2 微交互统一
E3-2 大文件拆分(3 文件 → 6 文件):
- daily-monitoring 449L → useDailyMonitoring.ts hook(238L) + 页面(255L)
- request.ts 376L → cache.ts(75L) + limiter.ts(32L) + 主文件(278L)
- BLEManager.ts 363L → BLEConnection.ts(212L) + 主文件(228L)

U3-2 微交互统一:
- 新增 haptic.ts 工具(light/medium/heavy 三级触觉反馈)
- PrimaryButton 点击触发 hapticLight()
- tokens.scss 新增 5 个动画时序 token(duration/easing)
- mixins.scss 新增 fade-in() mixin(支持 fast/normal/slow 三档)
2026-05-22 08:41:12 +08:00

239 lines
8.2 KiB
TypeScript

import { useState } from 'react';
import Taro, { useDidShow } from '@tarojs/taro';
import { validateNum } from '@/utils/validate';
import { createDailyMonitoring } from '@/services/health';
import { useAuthStore } from '@/stores/auth';
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 {
BP_RANGE, WEIGHT_RANGE, SUGAR_RANGE, VOLUME_RANGE,
checkAbnormal, formatDate, FIELD_LABELS,
type SectionKey,
} from './constants';
export function useDailyMonitoring() {
const currentPatient = useAuthStore((s) => s.currentPatient);
const today = formatDate(new Date());
const [dateIdx, setDateIdx] = useState(0);
const [dateList] = useState(() => {
const list: string[] = [];
for (let i = 0; i < 30; i++) {
const d = new Date();
d.setDate(d.getDate() - i);
list.push(formatDate(d));
}
return list;
});
const recordDate = dateList[dateIdx];
const [morningSystolic, setMorningSystolic] = useState('');
const [morningDiastolic, setMorningDiastolic] = useState('');
const [eveningSystolic, setEveningSystolic] = useState('');
const [eveningDiastolic, setEveningDiastolic] = useState('');
const [weight, setWeight] = useState('');
const [bloodSugar, setBloodSugar] = useState('');
const [fluidIntake, setFluidIntake] = useState('');
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>>({
morning: false,
evening: false,
other: true,
});
const toggleSection = (key: SectionKey) => {
setCollapsed(prev => ({ ...prev, [key]: !prev[key] }));
};
useDidShow(() => {
Taro.setNavigationBarTitle({ title: '日常监测上报' });
});
const resetForm = () => {
setMorningSystolic('');
setMorningDiastolic('');
setEveningSystolic('');
setEveningDiastolic('');
setWeight('');
setBloodSugar('');
setFluidIntake('');
setUrineOutput('');
setNotes('');
};
// ── Abnormal field gathering for submit confirmation ──
const gatherAbnormalFields = (): string[] => {
const abnormalFields: string[] = [];
const checks: Array<[string, string]> = [
['morningSystolic', morningSystolic],
['morningDiastolic', morningDiastolic],
['eveningSystolic', eveningSystolic],
['eveningDiastolic', eveningDiastolic],
['bloodSugar', bloodSugar],
];
for (const [field, value] of checks) {
const result = checkAbnormal(value, field);
if (result.abnormal) {
abnormalFields.push(FIELD_LABELS[field]);
}
}
return abnormalFields;
};
const handleSubmit = async () => {
if (!currentPatient) {
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
return;
}
const hasData =
morningSystolic || morningDiastolic ||
eveningSystolic || eveningDiastolic ||
weight || bloodSugar || fluidIntake || urineOutput;
if (!hasData) {
Taro.showToast({ title: '请至少填写一项数据', icon: 'none' });
return;
}
if ((morningSystolic && !morningDiastolic) || (!morningSystolic && morningDiastolic)) {
Taro.showToast({ title: '晨起血压请同时填写收缩压和舒张压', icon: 'none' });
return;
}
if ((eveningSystolic && !eveningDiastolic) || (!eveningSystolic && eveningDiastolic)) {
Taro.showToast({ title: '晚间血压请同时填写收缩压和舒张压', icon: 'none' });
return;
}
const parseNum = (v: string) => v ? parseFloat(v) : undefined;
const fields = {
morningSystolic: parseNum(morningSystolic),
morningDiastolic: parseNum(morningDiastolic),
eveningSystolic: parseNum(eveningSystolic),
eveningDiastolic: parseNum(eveningDiastolic),
weight: parseNum(weight),
bloodSugar: parseNum(bloodSugar),
fluidIntake: parseNum(fluidIntake),
urineOutput: parseNum(urineOutput),
};
const validations: Array<[number | undefined, string, typeof BP_RANGE]> = [
[fields.morningSystolic, '晨起收缩压', BP_RANGE],
[fields.morningDiastolic, '晨起舒张压', BP_RANGE],
[fields.eveningSystolic, '晚间收缩压', BP_RANGE],
[fields.eveningDiastolic, '晚间舒张压', BP_RANGE],
[fields.weight, '体重', WEIGHT_RANGE],
[fields.bloodSugar, '血糖', SUGAR_RANGE],
[fields.fluidIntake, '饮水量', VOLUME_RANGE],
[fields.urineOutput, '尿量', VOLUME_RANGE],
];
for (const [value, label, range] of validations) {
const err = validateNum(value, label, range);
if (err) {
Taro.showToast({ title: err, icon: 'none' });
return;
}
}
// ── Pre-submit abnormal confirmation ──
const abnormalFields = gatherAbnormalFields();
if (abnormalFields.length > 0) {
const confirmed = await Taro.showModal({
title: '数值异常提醒',
content: `以下指标超出正常范围:${abnormalFields.join('、')}。确认提交?`,
confirmText: '确认提交',
cancelText: '返回修改',
});
if (!confirmed.confirm) return;
}
setSubmitting(true);
try {
await createDailyMonitoring({
patient_id: currentPatient.id,
record_date: recordDate,
morning_bp_systolic: morningSystolic ? parseFloat(morningSystolic) : undefined,
morning_bp_diastolic: morningDiastolic ? parseFloat(morningDiastolic) : undefined,
evening_bp_systolic: eveningSystolic ? parseFloat(eveningSystolic) : undefined,
evening_bp_diastolic: eveningDiastolic ? parseFloat(eveningDiastolic) : undefined,
weight: weight ? parseFloat(weight) : undefined,
blood_sugar: bloodSugar ? parseFloat(bloodSugar) : undefined,
fluid_intake: fluidIntake ? parseFloat(fluidIntake) : undefined,
urine_output: urineOutput ? parseFloat(urineOutput) : undefined,
notes: notes || undefined,
});
trackEvent('daily_monitoring_submit', { date: recordDate });
useHealthStore.getState().clearCache();
clearRequestCache('/health/');
usePointsStore.getState().invalidate();
Taro.showToast({ title: '上报成功', icon: 'success' });
safeSetTimeout(() => {
Taro.showToast({ title: '+10 健康积分', icon: 'none', duration: 1500 });
}, 1600);
safeSetTimeout(() => {
Taro.navigateBack();
}, 3200);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : '上报失败';
if (msg.includes('已有记录') || msg.includes('already exists')) {
Taro.showModal({
title: '提示',
content: '该日期已有监测记录,请选择其他日期',
showCancel: false,
});
} else {
Taro.showToast({ title: msg, icon: 'none' });
}
} finally {
setSubmitting(false);
}
};
const isToday = recordDate === today;
// ── Abnormal state helpers for rendering ──
const morningSysAbnormal = checkAbnormal(morningSystolic, 'systolic');
const morningDiaAbnormal = checkAbnormal(morningDiastolic, 'diastolic');
const eveningSysAbnormal = checkAbnormal(eveningSystolic, 'systolic');
const eveningDiaAbnormal = checkAbnormal(eveningDiastolic, 'diastolic');
const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar');
return {
// Date state
dateIdx, setDateIdx, dateList, recordDate, isToday,
// Form field state
morningSystolic, setMorningSystolic,
morningDiastolic, setMorningDiastolic,
eveningSystolic, setEveningSystolic,
eveningDiastolic, setEveningDiastolic,
weight, setWeight,
bloodSugar, setBloodSugar,
fluidIntake, setFluidIntake,
urineOutput, setUrineOutput,
notes, setNotes,
// UI state
submitting, collapsed, toggleSection,
// Handlers
handleSubmit, resetForm,
// Abnormal indicators
morningSysAbnormal, morningDiaAbnormal,
eveningSysAbnormal, eveningDiaAbnormal,
bloodSugarAbnormal,
};
}