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 三档)
239 lines
8.2 KiB
TypeScript
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,
|
|
};
|
|
}
|