Files
hms/apps/miniprogram/src/pages/health/input/index.tsx
iven 030afb8213
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(miniprogram): 埋点事件追踪服务
- 新增 analytics.ts:trackEvent/trackPageView/flushEvents
- 事件队列本地缓存,批量上报到 /analytics/batch
- 首页 page_view、预约创建、随访提交、健康数据录入四个关键埋点
2026-04-24 13:02:08 +08:00

159 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}