- 新增 analytics.ts:trackEvent/trackPageView/flushEvents - 事件队列本地缓存,批量上报到 /analytics/batch - 首页 page_view、预约创建、随访提交、健康数据录入四个关键埋点
159 lines
5.7 KiB
TypeScript
159 lines
5.7 KiB
TypeScript
import { useState } from 'react';
|
||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||
import Taro from '@tarojs/taro';
|
||
import { z } from 'zod';
|
||
import { inputVitalSign } from '../../../services/health';
|
||
import { useAuthStore } from '../../../stores/auth';
|
||
import { useHealthStore } from '@/stores/health';
|
||
import { trackEvent } from '@/services/analytics';
|
||
import './index.scss';
|
||
|
||
const INDICATORS = [
|
||
{ value: 'blood_pressure', label: '血压 (mmHg)' },
|
||
{ value: 'heart_rate', label: '心率 (bpm)' },
|
||
{ value: 'blood_sugar_fasting', label: '空腹血糖 (mmol/L)' },
|
||
{ value: 'blood_sugar_postprandial', label: '餐后血糖 (mmol/L)' },
|
||
{ value: 'weight', label: '体重 (kg)' },
|
||
{ value: 'temperature', label: '体温 (℃)' },
|
||
];
|
||
|
||
const vitalSignSchema = z.object({
|
||
indicator_type: z.enum(['blood_pressure', 'heart_rate', 'blood_sugar_fasting', 'blood_sugar_postprandial', 'weight', 'temperature']),
|
||
value: z.number().positive({ message: '请输入有效数值' }),
|
||
extra: z.object({
|
||
systolic: z.number().min(60, '收缩压过低').max(250, '收缩压过高,请及时就医').optional(),
|
||
diastolic: z.number().min(40, '舒张压过低').max(150, '舒张压过高,请及时就医').optional(),
|
||
}).optional(),
|
||
note: z.string().max(200, '备注不能超过200字').optional(),
|
||
});
|
||
|
||
const WARN_THRESHOLDS: Record<string, { max?: number; min?: number; warning: string }> = {
|
||
blood_pressure: { max: 180, warning: '收缩压偏高,建议及时就医' },
|
||
heart_rate: { max: 120, min: 50, warning: '心率异常,请注意休息' },
|
||
blood_sugar_fasting: { max: 11.0, warning: '血糖偏高,建议就医检查' },
|
||
};
|
||
|
||
export default function HealthInput() {
|
||
const [indicatorIdx, setIndicatorIdx] = useState(0);
|
||
const [value, setValue] = useState('');
|
||
const [systolic, setSystolic] = useState('');
|
||
const [diastolic, setDiastolic] = useState('');
|
||
const [note, setNote] = useState('');
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const { currentPatient } = useAuthStore();
|
||
const { clearCache } = useHealthStore();
|
||
|
||
const handleSubmit = async () => {
|
||
if (!currentPatient) {
|
||
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
const currentIndicator = INDICATORS[indicatorIdx].value;
|
||
|
||
const input = currentIndicator === 'blood_pressure'
|
||
? { indicator_type: 'blood_pressure' as const, value: parseFloat(systolic), extra: { systolic: parseFloat(systolic), diastolic: parseFloat(diastolic) } }
|
||
: { indicator_type: currentIndicator as any, value: parseFloat(value) };
|
||
|
||
const result = vitalSignSchema.safeParse(input);
|
||
if (!result.success) {
|
||
Taro.showToast({ title: result.error.errors[0].message, icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
const threshold = WARN_THRESHOLDS[currentIndicator];
|
||
if (threshold) {
|
||
const val = input.value;
|
||
if ((threshold.max && val > threshold.max) || (threshold.min && val < threshold.min)) {
|
||
await Taro.showModal({ title: '健康提示', content: threshold.warning, showCancel: false });
|
||
}
|
||
}
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
await inputVitalSign(currentPatient.id, {
|
||
...input,
|
||
note: note || undefined,
|
||
});
|
||
clearCache();
|
||
Taro.showToast({ title: '录入成功', icon: 'success' });
|
||
trackEvent('health_data_input', { type: indicatorType });
|
||
setTimeout(() => Taro.navigateBack(), 1000);
|
||
} catch (e: unknown) {
|
||
const msg = e instanceof Error ? e.message : '录入失败';
|
||
Taro.showToast({ title: msg, icon: 'none' });
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<View className='input-page'>
|
||
<View className='input-section'>
|
||
<Text className='input-label'>指标类型</Text>
|
||
<Picker
|
||
mode='selector'
|
||
range={INDICATORS.map((i) => i.label)}
|
||
value={indicatorIdx}
|
||
onChange={(e) => setIndicatorIdx(Number(e.detail.value))}
|
||
>
|
||
<View className='input-picker'>
|
||
<Text>{INDICATORS[indicatorIdx].label}</Text>
|
||
<Text className='picker-arrow'>▾</Text>
|
||
</View>
|
||
</Picker>
|
||
</View>
|
||
|
||
{INDICATORS[indicatorIdx].value === 'blood_pressure' ? (
|
||
<>
|
||
<View className='input-section'>
|
||
<Text className='input-label'>收缩压</Text>
|
||
<Input
|
||
type='digit'
|
||
className='input-field'
|
||
placeholder='如 120'
|
||
value={systolic}
|
||
onInput={(e) => setSystolic(e.detail.value)}
|
||
/>
|
||
</View>
|
||
<View className='input-section'>
|
||
<Text className='input-label'>舒张压</Text>
|
||
<Input
|
||
type='digit'
|
||
className='input-field'
|
||
placeholder='如 80'
|
||
value={diastolic}
|
||
onInput={(e) => setDiastolic(e.detail.value)}
|
||
/>
|
||
</View>
|
||
</>
|
||
) : (
|
||
<View className='input-section'>
|
||
<Text className='input-label'>数值</Text>
|
||
<Input
|
||
type='digit'
|
||
className='input-field'
|
||
placeholder='请输入数值'
|
||
value={value}
|
||
onInput={(e) => setValue(e.detail.value)}
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
<View className='input-section'>
|
||
<Text className='input-label'>备注(可选)</Text>
|
||
<Input
|
||
className='input-field'
|
||
placeholder='如:饭后2小时'
|
||
value={note}
|
||
onInput={(e) => setNote(e.detail.value)}
|
||
/>
|
||
</View>
|
||
|
||
<View className='input-submit' onClick={submitting ? undefined : handleSubmit}>
|
||
<Text className='submit-text'>{submitting ? '提交中...' : '提交'}</Text>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|