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 三档)
This commit is contained in:
@@ -1,223 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
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 { useElderClass } from '@/hooks/useElderClass';
|
||||
import PageShell from '@/components/ui/PageShell';
|
||||
import ContentCard from '@/components/ui/ContentCard';
|
||||
import {
|
||||
BP_RANGE, WEIGHT_RANGE, SUGAR_RANGE, VOLUME_RANGE,
|
||||
checkAbnormal, formatDate, FIELD_LABELS,
|
||||
type SectionKey,
|
||||
} from './constants';
|
||||
import { useDailyMonitoring } from './useDailyMonitoring';
|
||||
import './index.scss';
|
||||
|
||||
export default function DailyMonitoring() {
|
||||
const modeClass = useElderClass();
|
||||
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');
|
||||
const {
|
||||
dateIdx, setDateIdx, dateList, recordDate, isToday,
|
||||
morningSystolic, setMorningSystolic,
|
||||
morningDiastolic, setMorningDiastolic,
|
||||
eveningSystolic, setEveningSystolic,
|
||||
eveningDiastolic, setEveningDiastolic,
|
||||
weight, setWeight,
|
||||
bloodSugar, setBloodSugar,
|
||||
fluidIntake, setFluidIntake,
|
||||
urineOutput, setUrineOutput,
|
||||
notes, setNotes,
|
||||
submitting, collapsed, toggleSection,
|
||||
handleSubmit, resetForm,
|
||||
morningSysAbnormal, morningDiaAbnormal,
|
||||
eveningSysAbnormal, eveningDiaAbnormal,
|
||||
bloodSugarAbnormal,
|
||||
} = useDailyMonitoring();
|
||||
|
||||
return (
|
||||
<PageShell padding="none" safeBottom className={modeClass}>
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user