feat(miniprogram): 小程序设备数据集成打通 — Phase 3
- 首页设备入口简化为直接跳转按钮(去除硬编码 never 状态) - 体征录入页增加「从设备同步」入口,设备数据自动回填表单 - 设备同步页支持 returnTo 参数,完成后返回录入页 - 医护工作台增加告警中心固定导航入口(带数字角标)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { useDidShow } from '@tarojs/taro';
|
||||
import Taro, { useDidShow, useRouter } from '@tarojs/taro';
|
||||
import { BLEManager } from '@/services/ble/BLEManager';
|
||||
import { XiaomiBandAdapter } from '@/services/ble/adapters/XiaomiBandAdapter';
|
||||
import { BloodPressureAdapter } from '@/services/ble/adapters/BloodPressureAdapter';
|
||||
@@ -19,6 +19,8 @@ type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' |
|
||||
|
||||
export default function DeviceSync() {
|
||||
const { currentPatient } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const returnTo = router.params.returnTo || '';
|
||||
const [pageState, setPageState] = useState<PageState>('idle');
|
||||
const [devices, setDevices] = useState<BLEDevice[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<BLEDevice | null>(null);
|
||||
@@ -86,6 +88,27 @@ export default function DeviceSync() {
|
||||
if (result.success) {
|
||||
setSyncCount(result.uploadedCount);
|
||||
setPageState('done');
|
||||
|
||||
// 如果从体征录入页跳转而来,将最新读数写入 storage 供回填
|
||||
if (returnTo === 'input' && liveReadings.length > 0) {
|
||||
const mapped: Record<string, number> = {};
|
||||
for (const r of liveReadings) {
|
||||
if (r.device_type === 'blood_pressure') {
|
||||
if (r.metric === 'systolic' && typeof r.values.value === 'number') mapped.systolic = r.values.value;
|
||||
if (r.metric === 'diastolic' && typeof r.values.value === 'number') mapped.diastolic = r.values.value;
|
||||
// 兼容 values 中直接包含 systolic/diastolic 的格式
|
||||
if (typeof r.values.systolic === 'number') mapped.systolic = r.values.systolic as number;
|
||||
if (typeof r.values.diastolic === 'number') mapped.diastolic = r.values.diastolic as number;
|
||||
} else if (r.device_type === 'blood_glucose' && typeof r.values.blood_glucose === 'number') {
|
||||
mapped.blood_sugar = r.values.blood_glucose as number;
|
||||
} else if (r.device_type === 'heart_rate' && typeof r.values.heart_rate === 'number') {
|
||||
mapped.heart_rate = r.values.heart_rate as number;
|
||||
}
|
||||
}
|
||||
if (Object.keys(mapped).length > 0) {
|
||||
Taro.setStorageSync('device_sync_result', JSON.stringify(mapped));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorMsg(result.error || '同步失败');
|
||||
setPageState('error');
|
||||
@@ -94,7 +117,7 @@ export default function DeviceSync() {
|
||||
setErrorMsg(e.message || '同步失败');
|
||||
setPageState('error');
|
||||
}
|
||||
}, [currentPatient, selectedDevice]);
|
||||
}, [currentPatient, selectedDevice, liveReadings, returnTo]);
|
||||
|
||||
const handleDisconnect = useCallback(async () => {
|
||||
await bleManager.disconnect();
|
||||
@@ -189,8 +212,13 @@ export default function DeviceSync() {
|
||||
<Text className="sync-result-title">同步完成</Text>
|
||||
<Text className="sync-result-count">成功上传 {syncCount} 条数据</Text>
|
||||
</View>
|
||||
<View className="sync-action" onClick={handleDisconnect}>
|
||||
<Text className="sync-action-text">完成</Text>
|
||||
<View className="sync-action" onClick={() => {
|
||||
handleDisconnect();
|
||||
if (returnTo === 'input') {
|
||||
Taro.navigateBack();
|
||||
}
|
||||
}}>
|
||||
<Text className="sync-action-text">{returnTo === 'input' ? '返回录入' : '完成'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -174,9 +174,30 @@
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__icon-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -12px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
background: $dan;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
border-radius: $r-pill;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 24px;
|
||||
color: $tx2;
|
||||
|
||||
@@ -30,7 +30,7 @@ const QUICK_ACTIONS = [
|
||||
{ label: '化验审核', initial: '审', route: '/pages/doctor/report/index' },
|
||||
{ label: '患者查询', initial: '查', route: '/pages/doctor/patients/index' },
|
||||
{ label: '随访记录', initial: '随', route: '/pages/doctor/followup/index' },
|
||||
{ label: '排班查看', initial: '排', route: '/pages/doctor/patients/index' },
|
||||
{ label: '告警中心', initial: '警', route: '/pages/doctor/alerts/index' },
|
||||
];
|
||||
|
||||
export default function DoctorHome() {
|
||||
@@ -143,7 +143,12 @@ export default function DoctorHome() {
|
||||
className='quick-action'
|
||||
onClick={() => Taro.navigateTo({ url: action.route })}
|
||||
>
|
||||
<Text className='quick-action__initial'>{action.initial}</Text>
|
||||
<View className='quick-action__icon-wrap'>
|
||||
<Text className='quick-action__initial'>{action.initial}</Text>
|
||||
{action.label === '告警中心' && alertCount > 0 && (
|
||||
<Text className='quick-action__badge'>{alertCount > 99 ? '99+' : alertCount}</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text className='quick-action__label'>{action.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -327,3 +327,63 @@
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ─── 设备快捷入口 ─── */
|
||||
.device-section {
|
||||
margin: 0 24px 24px;
|
||||
}
|
||||
|
||||
.device-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.device-entry-icon-wrap {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: $r;
|
||||
background: $pri-l;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-entry-icon-text {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.device-entry-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.device-entry-name {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.device-entry-desc {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.device-entry-arrow {
|
||||
font-size: 32px;
|
||||
color: $tx3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useState } from 'react';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import DeviceCard from '../../components/DeviceCard';
|
||||
import Loading from '../../components/Loading';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
@@ -123,18 +122,28 @@ export default function Index() {
|
||||
<Text className='greeting-date'>{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}</Text>
|
||||
</View>
|
||||
|
||||
{/* 设备快捷入口 */}
|
||||
{/* 设备快捷入口 — 点击直接跳转设备同步页面 */}
|
||||
<View className='device-section'>
|
||||
<DeviceCard
|
||||
deviceName='血压计'
|
||||
deviceType='blood_pressure'
|
||||
status='never'
|
||||
/>
|
||||
<DeviceCard
|
||||
deviceName='血糖仪'
|
||||
deviceType='blood_glucose'
|
||||
status='never'
|
||||
/>
|
||||
<View className='device-entry' onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index' })}>
|
||||
<View className='device-entry-icon-wrap'>
|
||||
<Text className='device-entry-icon-text'>{'\u{1FA7A}'}</Text>
|
||||
</View>
|
||||
<View className='device-entry-info'>
|
||||
<Text className='device-entry-name'>血压计</Text>
|
||||
<Text className='device-entry-desc'>蓝牙同步 · 自动采集</Text>
|
||||
</View>
|
||||
<Text className='device-entry-arrow'>{'›'}</Text>
|
||||
</View>
|
||||
<View className='device-entry' onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index' })}>
|
||||
<View className='device-entry-icon-wrap'>
|
||||
<Text className='device-entry-icon-text'>{'\u{1FA78}'}</Text>
|
||||
</View>
|
||||
<View className='device-entry-info'>
|
||||
<Text className='device-entry-name'>血糖仪</Text>
|
||||
<Text className='device-entry-desc'>蓝牙同步 · 自动采集</Text>
|
||||
</View>
|
||||
<Text className='device-entry-arrow'>{'›'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 今日健康 */}
|
||||
|
||||
232
apps/miniprogram/src/pages/pkg-health/input/index.scss
Normal file
232
apps/miniprogram/src/pages/pkg-health/input/index.scss
Normal file
@@ -0,0 +1,232 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.input-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 0 0 60px;
|
||||
}
|
||||
|
||||
/* ── hero ── */
|
||||
.input-hero {
|
||||
padding: 48px 32px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-hero-icon {
|
||||
@include flex-center;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: $r-lg;
|
||||
background: $pri-l;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-hero-icon-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.input-hero-title {
|
||||
@include section-title;
|
||||
font-size: 36px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-hero-sub {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ── sync entry ── */
|
||||
.input-sync-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: $shadow-sm;
|
||||
padding: 24px 28px;
|
||||
margin: 0 24px 20px;
|
||||
border: 1px dashed $pri;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.input-sync-entry-text {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.input-sync-entry-hint {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ── card ── */
|
||||
.input-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: $shadow-md;
|
||||
padding: 28px;
|
||||
margin: 0 24px 20px;
|
||||
}
|
||||
|
||||
.input-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-card-indicator {
|
||||
@include flex-center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: $r-sm;
|
||||
background: $acc-l;
|
||||
}
|
||||
|
||||
.input-card-indicator-char {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: $acc;
|
||||
}
|
||||
|
||||
.input-card-label {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
/* ── picker ── */
|
||||
.input-picker-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.input-picker-value {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
@include serif-number;
|
||||
}
|
||||
|
||||
.input-picker-arrow {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
transform: rotate(180deg);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── section title ── */
|
||||
.input-section-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── blood pressure group ── */
|
||||
.input-bp-group {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-bp-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-field-label {
|
||||
font-size: 22px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-bp-divider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 20px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-bp-line {
|
||||
width: 16px;
|
||||
height: 1px;
|
||||
background: $bd;
|
||||
}
|
||||
|
||||
.input-bp-slash {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 36px;
|
||||
color: $tx3;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* ── input field ── */
|
||||
.input-field-box {
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px 24px;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
@include serif-number;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-field-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-field-unit {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── submit ── */
|
||||
.input-submit {
|
||||
background: $pri;
|
||||
border-radius: $r;
|
||||
padding: 26px;
|
||||
text-align: center;
|
||||
margin: 48px 24px 0;
|
||||
box-shadow: $shadow-md;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.input-submit-disabled {
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-submit-text {
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
242
apps/miniprogram/src/pages/pkg-health/input/index.tsx
Normal file
242
apps/miniprogram/src/pages/pkg-health/input/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text, Input, Picker } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import { z } from 'zod';
|
||||
import { inputVitalSign } 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 './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();
|
||||
|
||||
/** 从 storage 中读取设备同步回传的数据并自动填充表单 */
|
||||
useDidShow(() => {
|
||||
try {
|
||||
const raw = Taro.getStorageSync('device_sync_result');
|
||||
if (!raw) return;
|
||||
Taro.removeStorageSync('device_sync_result');
|
||||
|
||||
const syncData: Record<string, number> = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
|
||||
// 字段映射:设备同步数据 → 表单字段
|
||||
if (syncData.systolic != null && syncData.diastolic != null) {
|
||||
// 有血压数据 → 切换到血压指标并填充
|
||||
setIndicatorIdx(0);
|
||||
setSystolic(String(syncData.systolic));
|
||||
setDiastolic(String(syncData.diastolic));
|
||||
} else if (syncData.blood_sugar != null) {
|
||||
setIndicatorIdx(2); // 空腹血糖
|
||||
setValue(String(syncData.blood_sugar));
|
||||
} else if (syncData.heart_rate != null) {
|
||||
setIndicatorIdx(1); // 心率
|
||||
setValue(String(syncData.heart_rate));
|
||||
}
|
||||
} catch {
|
||||
// 解析失败则忽略,不影响正常使用
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentPatient) {
|
||||
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndicator = INDICATORS[indicatorIdx].value;
|
||||
|
||||
if (currentIndicator === 'blood_pressure') {
|
||||
if (!systolic || !diastolic) {
|
||||
Taro.showToast({ title: '请填写收缩压和舒张压', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!value) {
|
||||
Taro.showToast({ title: '请输入数值', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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.issues[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();
|
||||
clearRequestCache('/health/');
|
||||
usePointsStore.getState().invalidate();
|
||||
Taro.showToast({ title: '录入成功', icon: 'success' });
|
||||
trackEvent('health_data_input', { type: currentIndicator });
|
||||
setTimeout(() => Taro.navigateBack(), 1000);
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : '录入失败';
|
||||
Taro.showToast({ title: msg, icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const indicatorInitial = INDICATORS[indicatorIdx].label.charAt(0);
|
||||
|
||||
return (
|
||||
<View className='input-page'>
|
||||
{/* 页面标题 */}
|
||||
<View className='input-hero'>
|
||||
<View className='input-hero-icon'>
|
||||
<Text className='input-hero-icon-text'>录</Text>
|
||||
</View>
|
||||
<Text className='input-hero-title'>体征录入</Text>
|
||||
<Text className='input-hero-sub'>记录今日健康数据</Text>
|
||||
</View>
|
||||
|
||||
{/* 从设备同步入口 */}
|
||||
<View className='input-sync-entry' onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index?returnTo=input' })}>
|
||||
<Text className='input-sync-entry-text'>从设备同步</Text>
|
||||
<Text className='input-sync-entry-hint'>蓝牙连接设备自动获取数据</Text>
|
||||
</View>
|
||||
|
||||
{/* 指标类型选择 */}
|
||||
<View className='input-card'>
|
||||
<View className='input-card-header'>
|
||||
<View className='input-card-indicator'>
|
||||
<Text className='input-card-indicator-char'>{indicatorInitial}</Text>
|
||||
</View>
|
||||
<Text className='input-card-label'>指标类型</Text>
|
||||
</View>
|
||||
<Picker
|
||||
mode='selector'
|
||||
range={INDICATORS.map((i) => i.label)}
|
||||
value={indicatorIdx}
|
||||
onChange={(e) => setIndicatorIdx(Number(e.detail.value))}
|
||||
>
|
||||
<View className='input-picker-row'>
|
||||
<Text className='input-picker-value'>{INDICATORS[indicatorIdx].label}</Text>
|
||||
<Text className='input-picker-arrow'>V</Text>
|
||||
</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
{/* 数值输入 */}
|
||||
{INDICATORS[indicatorIdx].value === 'blood_pressure' ? (
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>血压数值</Text>
|
||||
<View className='input-bp-group'>
|
||||
<View className='input-bp-field'>
|
||||
<Text className='input-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field-box'
|
||||
placeholder='如 120'
|
||||
value={systolic}
|
||||
onInput={(e) => setSystolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<View className='input-bp-divider'>
|
||||
<View className='input-bp-line' />
|
||||
<Text className='input-bp-slash'>/</Text>
|
||||
<View className='input-bp-line' />
|
||||
</View>
|
||||
<View className='input-bp-field'>
|
||||
<Text className='input-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field-box'
|
||||
placeholder='如 80'
|
||||
value={diastolic}
|
||||
onInput={(e) => setDiastolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='input-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>检测数值</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='input-field-box input-field-full'
|
||||
placeholder='请输入数值'
|
||||
value={value}
|
||||
onInput={(e) => setValue(e.detail.value)}
|
||||
/>
|
||||
<Text className='input-field-unit'>
|
||||
{INDICATORS[indicatorIdx].label.match(/\((.+)\)/)?.[1] || ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
<View className='input-card'>
|
||||
<Text className='input-section-title'>备注</Text>
|
||||
<Input
|
||||
className='input-field-box input-field-full'
|
||||
placeholder='如:饭后2小时(可选)'
|
||||
value={note}
|
||||
onInput={(e) => setNote(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 提交 */}
|
||||
<View
|
||||
className={`input-submit ${submitting ? 'input-submit-disabled' : ''}`}
|
||||
onClick={submitting ? undefined : handleSubmit}
|
||||
>
|
||||
<Text className='input-submit-text'>{submitting ? '提交中...' : '提交录入'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user