feat(health+miniprogram): 健康数据录入 + 趋势图
后端: - 新增 GET /health/vital-signs/trend 小程序趋势查询 API - 通过 JWT user_id 自动关联 patient,支持 range 参数 (7d/30d/90d) - 新增 MiniTrendQueryParams, MiniTrendResp, DataPoint DTO 前端: - 实现健康数据首页(今日概览 + 趋势入口 + 录入按钮) - 实现健康数据录入页(指标选择 + 数值输入 + 提交) - 实现趋势图页(时间范围切换 + 柱状图 + 数据列表) - 新增 health service 和 store(趋势缓存 + 今日摘要) - 修复所有页面相对路径引用问题
This commit is contained in:
59
apps/miniprogram/src/pages/health/input/index.scss
Normal file
59
apps/miniprogram/src/pages/health/input/index.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.input-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-picker {
|
||||
background: $card;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
color: $tx3;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
background: $card;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-submit {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.submit-text {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
93
apps/miniprogram/src/pages/health/input/index.tsx
Normal file
93
apps/miniprogram/src/pages/health/input/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { inputVitalSign } from '../../../services/health';
|
||||
import { useAuthStore } from '../../../stores/auth';
|
||||
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: '体温 (℃)' },
|
||||
];
|
||||
|
||||
export default function HealthInput() {
|
||||
const [indicatorIdx, setIndicatorIdx] = useState(0);
|
||||
const [value, setValue] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const { currentPatient } = useAuthStore();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!value) {
|
||||
Taro.showToast({ title: '请输入数值', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!currentPatient) {
|
||||
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await inputVitalSign(currentPatient.id, {
|
||||
indicator_type: INDICATORS[indicatorIdx].value,
|
||||
value: parseFloat(value),
|
||||
note: note || undefined,
|
||||
});
|
||||
Taro.showToast({ title: '录入成功', icon: 'success' });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch (e: any) {
|
||||
Taro.showToast({ title: e.message || '录入失败', 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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user