在移动应用开发中,健康监测类应用是一种重要的应用类型,需要考虑数据记录、状态判断、用户交互等多个方面。本文将深入分析一个功能完备的 React Native 血压监测应用实现,探讨其架构设计、状态管理、数据处理以及跨端兼容性策略。
组件化
该实现采用了清晰的单组件架构,主要包含以下部分:
- 主应用组件 (
BloodPressureApp) - 负责整体布局和状态管理 - 记录列表 - 展示血压记录列表
- 添加记录表单 - 用于添加新的血压记录
- 数据验证 - 确保输入数据的有效性
- 状态判断 - 根据血压值判断健康状态
这种架构设计使得代码结构清晰,易于维护。主应用组件负责管理全局状态和业务逻辑,而各个UI部分则负责具体的展示,实现了关注点分离。
状态管理
BloodPressureApp 组件使用 useState 钩子管理多个关键状态:
typescript
const [records, setRecords] = useState<BloodPressureRecord[]>([...]);
const [systolic, setSystolic] = useState<string>('');
const [diastolic, setDiastolic] = useState<string>('');
const [heartRate, setHeartRate] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [isAdding, setIsAdding] = useState<boolean>(false);
这种状态管理方式简洁高效,通过状态更新触发组件重新渲染,实现了血压记录的添加和展示。使用 TypeScript 类型定义确保了数据结构的类型安全,提高了代码的可靠性。
血压记录管理
应用实现了完整的血压记录管理功能:
- 记录展示 - 按时间倒序展示血压记录
- 添加记录 - 通过表单添加新的血压记录
- 数据验证 - 确保输入数据的有效性和合理性
- 状态判断 - 根据血压值自动判断健康状态
这种实现方式确保了用户可以方便地记录和查看血压数据,及时了解自己的健康状况。
数据验证
应用实现了严格的数据验证逻辑:
typescript
const addBloodPressureRecord = () => {
if (!systolic || !diastolic || !heartRate) {
Alert.alert('提示', '请填写完整的血压数据');
return;
}
const systolicNum = parseInt(systolic);
const diastolicNum = parseInt(diastolic);
const heartRateNum = parseInt(heartRate);
if (isNaN(systolicNum) || isNaN(diastolicNum) || isNaN(heartRateNum)) {
Alert.alert('提示', '请输入有效的数字');
return;
}
if (systolicNum < 50 || systolicNum > 300 || diastolicNum < 30 || diastolicNum > 200 || heartRateNum < 30 || heartRateNum > 200) {
Alert.alert('提示', '请输入合理的数值范围');
return;
}
// 创建新记录...
};
这种实现方式确保了输入数据的完整性、有效性和合理性,避免了错误数据的录入。
血压
应用实现了血压状态的自动判断:
typescript
const getBloodPressureStatus = (sys: number, dia: number): 'normal' | 'elevated' | 'high' | 'low' => {
if (sys < 90 || dia < 60) return 'low';
if (sys >= 140 || dia >= 90) return 'high';
if (sys >= 120 && sys <= 139) return 'elevated';
return 'normal';
};
这种实现方式根据医学标准自动判断血压状态,为用户提供了直观的健康状况反馈。
类型定义
该实现使用 TypeScript 定义了核心数据类型:
typescript
// 血压记录类型
type BloodPressureRecord = {
id: string;
systolic: number;
diastolic: number;
heartRate: number;
date: string;
time: string;
notes: string;
status: 'normal' | 'elevated' | 'high' | 'low';
};
这个类型定义包含了血压记录的完整信息,包括收缩压、舒张压、心率、日期、时间、备注和状态。这种类型定义使得数据结构更加清晰,提高了代码的可读性和可维护性,同时也提供了类型安全保障。
数据组织
应用数据按照功能模块进行组织:
- records - 血压记录列表
- systolic, diastolic, heartRate, notes - 表单输入数据
- isAdding - 是否显示添加表单的状态
这种数据组织方式使得数据管理更加清晰,易于扩展和维护。
布局结构
应用界面采用了清晰的层次结构:
- 顶部 - 显示应用标题和操作按钮
- 记录列表 - 显示血压记录列表
- 添加表单 - 用于添加新的血压记录
- 操作按钮 - 提供添加记录的入口
这种布局结构符合用户的使用习惯,用户可以快速了解应用内容并进行操作。
交互设计
应用实现了直观的交互设计:
- 添加记录 - 点击添加按钮显示表单
- 提交记录 - 填写表单后提交记录
- 取消添加 - 可以取消添加操作
- 数据验证 - 实时验证输入数据的有效性
- 状态反馈 - 通过颜色和文字反馈血压状态
这些交互设计元素共同构成了良好的用户体验,使得血压记录操作简单直观。
视觉设计
应用实现了丰富的视觉设计元素:
- 记录卡片 - 清晰展示血压记录信息
- 状态颜色 - 使用不同颜色表示不同的血压状态:
- 绿色 - 正常
- 橙色 - 偏高
- 红色 - 高血压
- 蓝色 - 低血压
- 数据格式 - 合理的布局和格式,提高数据的可读性
- 表单验证 - 及时的表单验证反馈
这些视觉设计元素使得用户界面更加美观,用户体验更加良好。
React Native 与鸿蒙跨端考虑
在设计跨端血压监测应用时,需要特别关注以下几个方面:
- 组件 API 兼容性 - 确保使用的 React Native 组件在鸿蒙系统上有对应实现
- 样式系统差异 - 不同平台对样式的支持程度不同,需要确保样式在两端都能正常显示
- 触摸事件处理 - 不同平台的触摸事件机制可能存在差异
- 图标系统 - 确保图标在不同平台上都能正常显示
- 数据处理 - 不同平台的数据处理性能可能存在差异
当前实现使用 ScrollView 渲染记录列表,对于大量记录可以考虑使用 FlatList:
typescript
// 优化前
<ScrollView style={styles.content}>
{records.map(record => (
<RecordCard key={record.id} record={record} />
))}
</ScrollView>
// 优化后
<FlatList
data={records}
renderItem={({ item }) => <RecordCard record={item} />}
keyExtractor={item => item.id}
style={styles.content}
/>
2. 状态管理
当前实现使用多个 useState 钩子管理状态,可以考虑使用 useReducer 或状态管理库来管理复杂状态:
typescript
// 优化前
const [records, setRecords] = useState<BloodPressureRecord[]>([...]);
const [systolic, setSystolic] = useState<string>('');
const [diastolic, setDiastolic] = useState<string>('');
const [heartRate, setHeartRate] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [isAdding, setIsAdding] = useState<boolean>(false);
// 优化后
type AppState = {
records: BloodPressureRecord[];
form: {
systolic: string;
diastolic: string;
heartRate: string;
notes: string;
};
isAdding: boolean;
};
type AppAction =
| { type: 'SET_RECORDS'; payload: BloodPressureRecord[] }
| { type: 'UPDATE_FORM'; payload: Partial<AppState['form']> }
| { type: 'SET_IS_ADDING'; payload: boolean }
| { type: 'ADD_RECORD'; payload: BloodPressureRecord }
| { type: 'RESET_FORM' };
const initialState: AppState = {
records: [...],
form: {
systolic: '',
diastolic: '',
heartRate: '',
notes: '',
},
isAdding: false,
};
const appReducer = (state: AppState, action: AppAction): AppState => {
switch (action.type) {
case 'SET_RECORDS':
return { ...state, records: action.payload };
case 'UPDATE_FORM':
return { ...state, form: { ...state.form, ...action.payload } };
case 'SET_IS_ADDING':
return { ...state, isAdding: action.payload };
case 'ADD_RECORD':
return { ...state, records: [action.payload, ...state.records] };
case 'RESET_FORM':
return { ...state, form: { systolic: '', diastolic: '', heartRate: '', notes: '' } };
default:
return state;
}
};
const [state, dispatch] = useReducer(appReducer, initialState);
3. 数据持久化
当前实现使用内存状态存储数据,可以考虑集成本地存储实现数据持久化:
typescript
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = '@blood_pressure_records';
const BloodPressureApp = () => {
const [records, setRecords] = useState<BloodPressureRecord[]>([]);
// 加载数据
useEffect(() => {
loadRecords();
}, []);
const loadRecords = async () => {
try {
const storedRecords = await AsyncStorage.getItem(STORAGE_KEY);
if (storedRecords) {
setRecords(JSON.parse(storedRecords));
}
} catch (error) {
console.error('加载数据失败:', error);
}
};
// 保存数据
const saveRecords = async (newRecords: BloodPressureRecord[]) => {
try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newRecords));
} catch (error) {
console.error('保存数据失败:', error);
}
};
// 添加记录时保存
const addBloodPressureRecord = () => {
// 验证逻辑...
const newRecord = { /* 新记录 */ };
const updatedRecords = [newRecord, ...records];
setRecords(updatedRecords);
saveRecords(updatedRecords);
// 其他逻辑...
};
// 其他代码...
};
4. 导航系统
可以集成 React Navigation 实现多页面导航:
typescript
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={BloodPressureApp}
options={{ title: '血压监测' }}
/>
<Stack.Screen
name="Statistics"
component={StatisticsScreen}
options={{ title: '统计分析' }}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{ title: '设置' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
本文深入分析了一个功能完备的 React Native 血压监测应用实现,从架构设计、状态管理、数据处理到跨端兼容性都进行了详细探讨。该实现不仅功能完整,而且代码结构清晰,具有良好的可扩展性和可维护性。
医疗健康类的血压记录应用在 React Native 中的完整实现逻辑,并掌握其向鸿蒙(HarmonyOS)平台跨端适配的核心方案。该应用是典型的医疗数据管理类应用,涵盖了表单验证、数据状态分类、动态样式渲染、列表数据管理等核心场景,是健康类应用跨端开发的典型案例。
1. 应用架构
该血压记录应用构建了符合医疗数据规范的强类型数据模型,为应用的专业性和可靠性奠定基础:
typescript
// 血压记录核心数据模型
type BloodPressureRecord = {
id: string; // 唯一标识
systolic: number; // 收缩压(高压)
diastolic: number; // 舒张压(低压)
heartRate: number; // 心率
date: string; // 测量日期
time: string; // 测量时间
notes: string; // 备注信息
status: 'normal' | 'elevated' | 'high' | 'low'; // 血压状态
};
数据设计亮点:
- 医疗数据标准化:字段命名符合医疗行业规范,收缩压/舒张压/心率的命名精准反映医学概念;
- 状态枚举化:使用联合类型限制血压状态,避免非法值,符合医疗数据严谨性要求;
- 时间维度完整:同时记录日期和时间,满足血压监测的时间追踪需求;
- 扩展字段合理:备注字段支持用户记录测量时的状态,提升数据实用性;
- 强类型约束:使用 TypeScript 接口确保数据类型安全,避免运行时错误。
2. 状态管理
应用采用 React Hooks 构建了分层的状态管理体系,区分数据状态和UI状态:
typescript
// 数据状态 - 血压记录列表
const [records, setRecords] = useState<BloodPressureRecord[]>([/* 初始数据 */]);
// UI状态 - 表单输入
const [systolic, setSystolic] = useState<string>('');
const [diastolic, setDiastolic] = useState<string>('');
const [heartRate, setHeartRate] = useState<string>('');
const [notes, setNotes] = useState<string>('');
// UI状态 - 表单显示控制
const [isAdding, setIsAdding] = useState<boolean>(false);
状态设计原则:
- 数据/UI分离:将核心数据(records)与表单UI状态分离,职责边界清晰;
- 输入状态类型合理:表单输入使用字符串类型,符合 TextInput 的值类型特性;
- 布尔状态简洁:isAdding 状态控制表单的显示/隐藏,逻辑清晰;
- 不可变更新:记录列表更新遵循 React 不可变原则,通过新数组替换实现;
- 初始值安全:所有状态均设置合理初始值,避免 undefined 问题。
(1)医疗数据验证
这是应用的核心业务逻辑,实现了符合医学规范的数据验证和状态判定:
typescript
// 添加新的血压记录(包含完整的数据验证)
const addBloodPressureRecord = () => {
// 1. 必填项验证
if (!systolic || !diastolic || !heartRate) {
Alert.alert('提示', '请填写完整的血压数据');
return;
}
// 2. 数值类型验证
const systolicNum = parseInt(systolic);
const diastolicNum = parseInt(diastolic);
const heartRateNum = parseInt(heartRate);
if (isNaN(systolicNum) || isNaN(diastolicNum) || isNaN(heartRateNum)) {
Alert.alert('提示', '请输入有效的数字');
return;
}
// 3. 医学范围验证(符合临床合理范围)
if (systolicNum < 50 || systolicNum > 300 ||
diastolicNum < 30 || diastolicNum > 200 ||
heartRateNum < 30 || heartRateNum > 200) {
Alert.alert('提示', '请输入合理的数值范围');
return;
}
// 4. 构建新记录(自动生成时间和状态)
const newRecord: BloodPressureRecord = {
id: `${records.length + 1}`,
systolic: systolicNum,
diastolic: diastolicNum,
heartRate: heartRateNum,
date: new Date().toISOString().split('T')[0], // 格式化日期
time: new Date().toTimeString().substring(0, 5), // 格式化时间
notes: notes || '无备注',
status: getBloodPressureStatus(systolicNum, diastolicNum) // 自动判定状态
};
// 5. 更新数据并重置表单
setRecords([newRecord, ...records]); // 新记录添加到顶部
resetForm();
setIsAdding(false);
Alert.alert('成功', '血压记录已保存');
};
// 根据血压值判断状态(符合医学标准)
const getBloodPressureStatus = (sys: number, dia: number): 'normal' | 'elevated' | 'high' | 'low' => {
if (sys < 90 || dia < 60) return 'low'; // 低血压
if (sys >= 140 || dia >= 90) return 'high'; // 高血压
if (sys >= 120 && sys <= 139) return 'elevated'; // 血压偏高
return 'normal'; // 正常血压
};
实现亮点:
- 多层级验证:必填项→类型→医学范围的三级验证,确保数据有效性;
- 医学标准合规:血压状态判定逻辑符合临床诊断标准;
- 合理范围限制:基于医学常识设置数值上下限,避免不合理数据录入;
- 时间自动生成:使用标准化的日期时间格式化,保证数据一致性;
- 用户体验优化:验证失败时给出明确提示,成功后反馈操作结果;
- 数据预处理:备注字段为空时自动填充默认值,避免空数据。
(2)动态样式渲染
根据血压状态动态渲染不同颜色的状态标签,提升数据可视化效果:
typescript
// 获取状态颜色(医疗级别的色彩编码)
const getStatusColor = (status: string): string => {
switch (status) {
case 'normal': return '#10b981'; // 绿色 - 正常
case 'elevated': return '#f59e0b';// 橙色 - 偏高
case 'high': return '#ef4444'; // 红色 - 高血压
case 'low': return '#3b82f6'; // 蓝色 - 低血压
default: return '#64748b'; // 灰色 - 未知
}
};
// 渲染状态标签
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>
{item.status === 'normal' ? '正常' :
item.status === 'elevated' ? '偏高' :
item.status === 'high' ? '高血压' : '低血压'}
</Text>
</View>
设计原则:
- 医疗色彩编码:采用符合医疗UI设计规范的色彩体系,绿色/红色分别对应正常/异常;
- 本地化显示:将英文状态转换为中文显示,提升用户体验;
- 样式动态绑定:通过样式数组实现状态与颜色的动态关联;
- 视觉层级清晰:使用圆角徽章样式突出显示血压状态,便于快速识别。
(3)表单交互
实现了完整的表单交互流程和数据增删操作:
typescript
// 重置表单
const resetForm = () => {
setSystolic('');
setDiastolic('');
setHeartRate('');
setNotes('');
};
// 删除记录(带确认弹窗)
const deleteRecord = (id: string) => {
setRecords(records.filter(record => record.id !== id));
};
// 表单显示/隐藏切换
<TouchableOpacity
style={styles.addButton}
onPress={() => setIsAdding(!isAdding)}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
// 删除确认弹窗
<TouchableOpacity
style={styles.deleteButton}
onPress={() => Alert.alert('删除', `确定要删除这条记录吗?`, [
{ text: '取消' },
{ text: '删除', onPress: () => deleteRecord(item.id) }
])}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
交互设计亮点:
- 表单重置机制:提交成功或取消操作时重置表单,避免残留数据;
- 删除确认:重要操作增加确认步骤,防止误删医疗数据;
- 表单切换动画:通过 isAdding 状态控制表单的显示/隐藏,交互流畅;
- 操作反馈:所有数据操作均有明确的弹窗提示,提升用户安全感;
- 键盘优化 :数值输入框设置
keyboardType="numeric",优化输入体验。
(1)医疗数据表单设计
tsx
<View style={styles.formContainer}>
<Text style={styles.formTitle}>添加血压记录</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>收缩压 (高压) *</Text>
<TextInput
style={styles.input}
value={systolic}
onChangeText={setSystolic}
placeholder="例如: 120"
keyboardType="numeric"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>舒张压 (低压) *</Text>
<TextInput
style={styles.input}
value={diastolic}
onChangeText={setDiastolic}
placeholder="例如: 80"
keyboardType="numeric"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>心率 *</Text>
<TextInput
style={styles.input}
value={heartRate}
onChangeText={setHeartRate}
placeholder="例如: 72"
keyboardType="numeric"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>备注</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={notes}
onChangeText={setNotes}
placeholder="记录时的状态或感受(可选)"
multiline
/>
</View>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setIsAdding(false);
resetForm();
}}
>
<Text style={styles.cancelButtonText}>取消</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.saveButton}
onPress={addBloodPressureRecord}
>
<Text style={styles.saveButtonText}>保存</Text>
</TouchableOpacity>
</View>
</View>
表单设计要点:
- 医疗表单规范:必填项标注*号,符合表单设计规范;
- 输入提示明确:placeholder 提供示例值,引导用户正确输入;
- 键盘优化:数值输入框使用数字键盘,提升输入效率;
- 多行文本支持 :备注字段支持多行输入,设置
textAlignVertical: 'top'优化排版; - 按钮布局合理:取消/保存按钮左右布局,符合用户操作习惯;
- 视觉分层:通过卡片容器、间距、阴影构建清晰的表单层级。
(2)血压记录卡片
tsx
const renderRecordItem = ({ item }: { item: BloodPressureRecord }) => (
<View style={styles.recordCard}>
<View style={styles.headerContainer}>
<Text style={styles.dateText}>{item.date}</Text>
<Text style={styles.timeText}>{item.time}</Text>
</View>
<View style={styles.pressureContainer}>
<View style={styles.pressureItem}>
<Text style={styles.pressureValue}>{item.systolic}</Text>
<Text style={styles.pressureLabel}>收缩压</Text>
</View>
<View style={styles.separator}>
<Text>/</Text>
</View>
<View style={styles.pressureItem}>
<Text style={styles.pressureValue}>{item.diastolic}</Text>
<Text style={styles.pressureLabel}>舒张压</Text>
</View>
<View style={styles.hrContainer}>
<Text style={styles.hrValue}>{item.heartRate}</Text>
<Text style={styles.hrLabel}>心率</Text>
</View>
</View>
<View style={styles.statusContainer}>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>
{item.status === 'normal' ? '正常' :
item.status === 'elevated' ? '偏高' :
item.status === 'high' ? '高血压' : '低血压'}
</Text>
</View>
</View>
<Text style={styles.notesText}>{item.notes}</Text>
<View style={styles.actionContainer}>
<TouchableOpacity
style={styles.editButton}
onPress={() => Alert.alert('编辑', `编辑血压记录 ID: ${item.id}`)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => Alert.alert('删除', `确定要删除这条记录吗?`, [
{ text: '取消' },
{ text: '删除', onPress: () => deleteRecord(item.id) }
])}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</View>
</View>
);
卡片设计亮点:
- 医疗数据可视化:收缩压/舒张压使用大号字体突出显示,符合医疗数据展示习惯;
- 分隔符设计:使用/符号分隔收缩压和舒张压,符合医学表示规范;
- 状态突出显示:血压状态使用色彩徽章,便于快速识别;
- 操作区合理:编辑/删除按钮放置在卡片底部右侧,不干扰数据阅读;
- 信息层级清晰:日期时间→核心数据→状态→备注→操作,符合信息阅读顺序。
5. 样式
该应用的样式系统体现了医疗类应用的专业设计原则:
typescript
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc', // 浅灰背景,降低视觉疲劳
},
recordCard: {
backgroundColor: '#ffffff', // 白色卡片
borderRadius: 12, // 大圆角设计,提升友好性
padding: 16,
marginBottom: 12,
// 跨平台阴影
elevation: 1, // Android阴影
shadowColor: '#000', // iOS阴影
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
statusBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12, // 胶囊形徽章
},
deleteButton: {
backgroundColor: '#fee2e2', // 浅红色背景
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
deleteButtonText: {
fontSize: 12,
color: '#ef4444', // 红色文字
fontWeight: '500',
},
// 其他样式...
});
样式设计原则:
- 医疗UI风格:简洁、专业、低饱和度的色彩体系,符合医疗应用设计规范;
- 视觉层次清晰:通过阴影、圆角、间距构建卡片层级,提升可读性;
- 交互反馈明确:删除按钮使用浅红色背景,视觉上提示风险操作;
- 响应式布局:使用 flex 布局适配不同屏幕尺寸;
- 平台兼容:同时设置 elevation 和 shadow 实现跨平台一致的阴影效果;
- 文本层级:通过字体大小、粗细、颜色区分不同重要程度的信息。
将该 React Native 血压记录应用适配到鸿蒙平台,核心是将 React 的表单处理、动态样式、数据验证、列表渲染等核心能力映射到鸿蒙 ArkTS + ArkUI 生态,以下是完整的适配方案。
1. 核心技术栈映射
| React Native 核心能力 | 鸿蒙 ArkTS 对应实现 | 适配关键说明 |
|---|---|---|
useState 状态管理 |
@State/@Link/@DerivedState |
基础状态+派生状态组合使用 |
TextInput 表单输入 |
TextInput 组件 + onChange |
输入事件和值绑定适配 |
| 动态样式数组 | 条件样式 + @Styles 装饰器 |
状态驱动的样式适配 |
Alert.alert 弹窗 |
AlertDialog 组件 |
交互弹窗替换 |
| 列表渲染(map) | ForEach + List 组件 |
高性能列表渲染 |
TouchableOpacity |
Button + stateEffect(true) |
可点击组件替换 |
StyleSheet.create |
@Styles/@Extend + 链式样式 |
样式体系重构 |
| 数值验证逻辑 | 完全复用(TypeScript 逻辑) | 业务逻辑零修改 |
multiline 多行输入 |
TextInput + type: InputType.Multiline |
多行输入适配 |
keyboardType="numeric" |
type: InputType.Number |
数字键盘适配 |
2. 鸿蒙端
tsx
// index.ets - 鸿蒙端血压记录应用完整实现
import { BusinessError } from '@ohos.base';
// 类型定义(与RN端完全一致)
type BloodPressureRecord = {
id: string;
systolic: number;
diastolic: number;
heartRate: number;
date: string;
time: string;
notes: string;
status: 'normal' | 'elevated' | 'high' | 'low';
};
@Entry
@Component
struct BloodPressureApp {
// 基础状态(对应RN的useState)
@State records: BloodPressureRecord[] = [
{
id: '1',
systolic: 120,
diastolic: 80,
heartRate: 72,
date: '2023-05-15',
time: '08:30',
notes: '早晨测量,感觉良好',
status: 'normal'
},
{
id: '2',
systolic: 140,
diastolic: 90,
heartRate: 85,
date: '2023-05-14',
time: '14:20',
notes: '下午测量,有些紧张',
status: 'high'
},
{
id: '3',
systolic: 110,
diastolic: 70,
heartRate: 68,
date: '2023-05-13',
time: '20:15',
notes: '晚上测量,放松状态下',
status: 'normal'
}
];
@State systolic: string = '';
@State diastolic: string = '';
@State heartRate: string = '';
@State notes: string = '';
@State isAdding: boolean = false;
// 通用样式封装 - 卡片容器样式
@Styles
cardStyle() {
.backgroundColor('#ffffff')
.borderRadius(12)
.shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
}
// 通用样式封装 - 输入框样式
@Styles
inputStyle() {
.backgroundColor('#f1f5f9')
.borderRadius(8)
.paddingVertical(12)
.paddingHorizontal(12)
.fontSize(16)
.fontColor('#1e293b');
}
// 获取状态颜色(完全复用RN端逻辑)
private getStatusColor(status: string): string {
switch (status) {
case 'normal': return '#10b981';
case 'elevated': return '#f59e0b';
case 'high': return '#ef4444';
case 'low': return '#3b82f6';
default: return '#64748b';
}
}
// 根据血压值判断状态(完全复用RN端逻辑)
private getBloodPressureStatus(sys: number, dia: number): 'normal' | 'elevated' | 'high' | 'low' {
if (sys < 90 || dia < 60) return 'low';
if (sys >= 140 || dia >= 90) return 'high';
if (sys >= 120 && sys <= 139) return 'elevated';
return 'normal';
}
// 重置表单(完全复用RN端逻辑)
private resetForm() {
this.systolic = '';
this.diastolic = '';
this.heartRate = '';
this.notes = '';
}
// 添加新的血压记录(复用核心逻辑,适配鸿蒙弹窗)
private addBloodPressureRecord() {
if (!this.systolic || !this.diastolic || !this.heartRate) {
AlertDialog.show({
title: '提示',
message: '请填写完整的血压数据',
confirm: { value: '确定' }
});
return;
}
const systolicNum = parseInt(this.systolic);
const diastolicNum = parseInt(this.diastolic);
const heartRateNum = parseInt(this.heartRate);
if (isNaN(systolicNum) || isNaN(diastolicNum) || isNaN(heartRateNum)) {
AlertDialog.show({
title: '提示',
message: '请输入有效的数字',
confirm: { value: '确定' }
});
return;
}
if (systolicNum < 50 || systolicNum > 300 ||
diastolicNum < 30 || diastolicNum > 200 ||
heartRateNum < 30 || heartRateNum > 200) {
AlertDialog.show({
title: '提示',
message: '请输入合理的数值范围',
confirm: { value: '确定' }
});
return;
}
const newRecord: BloodPressureRecord = {
id: `${this.records.length + 1}`,
systolic: systolicNum,
diastolic: diastolicNum,
heartRate: heartRateNum,
date: new Date().toISOString().split('T')[0],
time: new Date().toTimeString().substring(0, 5),
notes: this.notes || '无备注',
status: this.getBloodPressureStatus(systolicNum, diastolicNum)
};
this.records = [newRecord, ...this.records];
this.resetForm();
this.isAdding = false;
AlertDialog.show({
title: '成功',
message: '血压记录已保存',
confirm: { value: '确定' }
});
}
// 删除记录(适配鸿蒙确认弹窗)
private deleteRecord(id: string) {
this.records = this.records.filter(record => record.id !== id);
}
// 渲染血压记录项
@Builder
renderRecordItem(item: BloodPressureRecord) {
Column({ space: 12 }) {
// 日期时间头部
Row({ space: 0 }) {
Text(item.date)
.fontSize(14)
.fontColor('#64748b')
.fontWeight(FontWeight.Medium);
Text(item.time)
.fontSize(14)
.fontColor('#64748b')
.marginLeft('auto');
}
// 血压和心率数据
Row({ space: 0, alignItems: ItemAlign.Center }) {
// 收缩压
Column({ space: 4, flex: 1 }) {
Text(`${item.systolic}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('收缩压')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
// 分隔符
Text('/')
.fontSize(16)
.fontColor('#64748b')
.width(20)
.textAlign(TextAlign.Center);
// 舒张压
Column({ space: 4, flex: 1 }) {
Text(`${item.diastolic}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('舒张压')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
// 心率
Column({ space: 4, flex: 1 }) {
Text(`${item.heartRate}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('心率')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
}
// 状态徽章
Row({ space: 0 }) {
Text(item.status === 'normal' ? '正常' :
item.status === 'elevated' ? '偏高' :
item.status === 'high' ? '高血压' : '低血压')
.fontSize(12)
.fontColor('#ffffff')
.fontWeight(FontWeight.Medium)
.backgroundColor(this.getStatusColor(item.status))
.paddingHorizontal(12)
.paddingVertical(4)
.borderRadius(12);
}
// 备注信息
Text(item.notes)
.fontSize(14)
.fontColor('#64748b')
.width('100%');
// 操作按钮
Row({ space: 8, justifyContent: FlexAlign.End }) {
Button('编辑')
.backgroundColor('#f1f5f9')
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.fontSize(12)
.fontColor('#3b82f6')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '编辑',
message: `编辑血压记录 ID: ${item.id}`,
confirm: { value: '确定' }
});
});
Button('删除')
.backgroundColor('#fee2e2')
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.fontSize(12)
.fontColor('#ef4444')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '删除',
message: '确定要删除这条记录吗?',
confirm: {
value: '删除',
action: () => this.deleteRecord(item.id)
},
cancel: { value: '取消' }
});
});
}
.width('100%');
}
.cardStyle()
.padding(16)
.marginBottom(12)
.width('100%');
}
// 渲染添加表单
@Builder
renderAddForm() {
Column({ space: 16 }) {
Text('添加血压记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.width('100%');
// 收缩压输入
Column({ space: 8 }) {
Text('收缩压 (高压) *')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1e293b')
.width('100%');
TextInput({
placeholder: '例如: 120',
text: this.systolic,
type: InputType.Number // 对应RN的keyboardType="numeric"
})
.inputStyle()
.onChange((value) => {
this.systolic = value;
});
}
.width('100%');
// 舒张压输入
Column({ space: 8 }) {
Text('舒张压 (低压) *')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1e293b')
.width('100%');
TextInput({
placeholder: '例如: 80',
text: this.diastolic,
type: InputType.Number
})
.inputStyle()
.onChange((value) => {
this.diastolic = value;
});
}
.width('100%');
// 心率输入
Column({ space: 8 }) {
Text('心率 *')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1e293b')
.width('100%');
TextInput({
placeholder: '例如: 72',
text: this.heartRate,
type: InputType.Number
})
.inputStyle()
.onChange((value) => {
this.heartRate = value;
});
}
.width('100%');
// 备注输入(多行)
Column({ space: 8 }) {
Text('备注')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#1e293b')
.width('100%');
TextInput({
placeholder: '记录时的状态或感受(可选)',
text: this.notes,
type: InputType.Multiline // 对应RN的multiline
})
.inputStyle()
.height(80)
.textAlignVertical(TextAlignVertical.Top) // 对应RN的textAlignVertical: 'top'
.onChange((value) => {
this.notes = value;
});
}
.width('100%');
// 按钮组
Row({ space: 8 }) {
Button('取消')
.flex(1)
.backgroundColor('#f1f5f9')
.padding(12)
.borderRadius(8)
.fontSize(16)
.fontColor('#64748b')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => {
this.isAdding = false;
this.resetForm();
});
Button('保存')
.flex(1)
.backgroundColor('#3b82f6')
.padding(12)
.borderRadius(8)
.fontSize(16)
.fontColor('#ffffff')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => this.addBloodPressureRecord());
}
.width('100%')
.marginTop(8);
}
.cardStyle()
.padding(16)
.marginBottom(16)
.width('100%');
}
build() {
Column({ space: 0 }) {
// 头部导航栏
this.Header();
// 内容区域(滚动容器)
Scroll() {
Column({ space: 16 }) {
// 搜索栏
this.SearchBar();
// 添加血压记录表单(条件渲染)
if (this.isAdding) {
this.renderAddForm();
}
// 记录列表标题
Row({ space: 0 }) {
Text('血压记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text(`${this.records.length} 条记录`)
.fontSize(14)
.fontColor('#64748b')
.marginLeft('auto');
}
.width('100%')
.marginBottom(12);
// 血压记录列表
List({ space: 0 }) {
ForEach(this.records, (item) => {
ListItem() {
this.renderRecordItem(item);
}
}, (item) => item.id);
}
.width('100%')
.shrink(0)
.edgeEffect(EdgeEffect.None)
.scrollBar(BarState.Off);
// 统计信息
this.StatsSection();
// 使用说明
this.InfoSection();
}
.padding(16)
.width('100%');
}
.flex(1)
.width('100%');
// 底部导航
this.BottomNav();
}
.width('100%')
.height('100%')
.backgroundColor('#f8fafc')
.safeArea(true);
}
// 头部导航栏 - Builder函数封装
@Builder
Header() {
Row({ space: 0 }) {
Text('血压记录')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
// 添加按钮
Button('+')
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#3b82f6')
.fontSize(20)
.fontColor('#ffffff')
.fontWeight(FontWeight.Bold)
.stateEffect(true)
.onClick(() => this.isAdding = !this.isAdding)
.marginLeft('auto');
}
.padding(20)
.backgroundColor('#ffffff')
.borderBottom({ width: 1, color: '#e2e8f0' })
.width('100%');
}
// 搜索栏 - Builder函数封装
@Builder
SearchBar() {
Row({ space: 12 }) {
Text('🔍')
.fontSize(18)
.fontColor('#64748b');
Text('搜索血压记录')
.fontSize(14)
.fontColor('#94a3b8')
.flex(1);
}
.cardStyle()
.paddingVertical(12)
.paddingHorizontal(16)
.borderRadius(20)
.width('100%');
}
// 统计信息 - Builder函数封装
@Builder
StatsSection() {
Row({ space: 0 }) {
// 最新收缩压
Column({ space: 4, flex: 1 }) {
Text(`${this.records.length > 0 ? this.records[0].systolic : '--'}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('最新收缩压')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
// 最新舒张压
Column({ space: 4, flex: 1 }) {
Text(`${this.records.length > 0 ? this.records[0].diastolic : '--'}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('最新舒张压')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
// 最新心率
Column({ space: 4, flex: 1 }) {
Text(`${this.records.length > 0 ? this.records[0].heartRate : '--'}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#3b82f6');
Text('最新心率')
.fontSize(12)
.fontColor('#64748b');
}
.alignItems(ItemAlign.Center);
}
.cardStyle()
.padding(16)
.width('100%');
}
// 使用说明 - Builder函数封装
@Builder
InfoSection() {
Column({ space: 8 }) {
Text('使用说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.marginBottom(4);
Text('• 点击+号添加新的血压记录')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 正常血压范围: 收缩压 90-140, 舒张压 60-90')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 建议每天定时测量血压')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 如有异常请及时就医')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
}
.cardStyle()
.padding(16)
.width('100%');
}
// 底部导航 - Builder函数封装
@Builder
BottomNav() {
Row({ space: 0 }) {
// 首页
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({ title: '首页', message: '首页功能' });
}) {
Column({ space: 4 }) {
Text('🏠')
.fontSize(20)
.fontColor('#94a3b8');
Text('首页')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 血压
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({ title: '血压', message: '血压管理' });
}) {
Column({ space: 4 }) {
Text('🩸')
.fontSize(20)
.fontColor('#94a3b8');
Text('血压')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 图表
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({ title: '图表', message: '数据统计' });
}) {
Column({ space: 4 }) {
Text('📊')
.fontSize(20)
.fontColor('#94a3b8');
Text('图表')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 我的
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({ title: '我的', message: '个人中心' });
}) {
Column({ space: 4 }) {
Text('👤')
.fontSize(20)
.fontColor('#94a3b8');
Text('我的')
.fontSize(12)
.fontColor('#94a3b8');
}
};
}
.backgroundColor('#ffffff')
.borderTop({ width: 1, color: '#e2e8f0' })
.paddingVertical(12)
.width('100%');
}
}
(1)医疗表单验证
React Native 的 TextInput 组件适配为鸿蒙的 TextInput 组件,重点优化了医疗数据输入体验:
tsx
// React Native - 数字输入
<TextInput
style={styles.input}
value={systolic}
onChangeText={setSystolic}
placeholder="例如: 120"
keyboardType="numeric"
/>
// 鸿蒙
TextInput({
placeholder: '例如: 120',
text: this.systolic,
type: InputType.Number // 数字键盘适配
})
.inputStyle()
.onChange((value) => {
this.systolic = value;
});
// React Native - 多行输入
<TextInput
style={[styles.input, styles.textArea]}
value={notes}
onChangeText={setNotes}
placeholder="记录时的状态或感受(可选)"
multiline
/>
// 鸿蒙
TextInput({
placeholder: '记录时的状态或感受(可选)',
text: this.notes,
type: InputType.Multiline // 多行输入
})
.inputStyle()
.height(80)
.textAlignVertical(TextAlignVertical.Top) // 文本居顶
.onChange((value) => {
this.notes = value;
});
适配优势:
- 输入类型精准映射 :
keyboardType="numeric"对应InputType.Number,保证数字输入体验一致; - 多行输入优化 :
multiline对应InputType.Multiline,并设置textAlignVertical保证文本排版一致; - 样式复用 :通过
@Styles封装inputStyle,统一所有输入框样式; - 事件处理简化 :鸿蒙的
onChange直接接收值参数,无需额外处理; - 验证逻辑复用:核心的医疗数据验证逻辑 100% 复用,仅适配弹窗提示方式。
(2)动态样式
React Native 的样式数组适配为鸿蒙的条件样式,保持医疗状态的可视化效果:
tsx
// React Native - 动态状态样式
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>
{item.status === 'normal' ? '正常' : '高血压'}
</Text>
</View>
// 鸿蒙
Text(item.status === 'normal' ? '正常' : '高血压')
.fontSize(12)
.fontColor('#ffffff')
.fontWeight(FontWeight.Medium)
.backgroundColor(this.getStatusColor(item.status)) // 条件样式
.paddingHorizontal(12)
.paddingVertical(4)
.borderRadius(12);
适配技巧:
- 链式样式调用:鸿蒙的链式样式调用替代 RN 的样式数组,代码更简洁;
- 颜色函数复用 :
getStatusColor函数完全复用,保证医疗色彩编码一致; - 状态文本映射:保持中文状态文本的本地化显示,符合医疗应用习惯;
- 视觉效果一致:相同的 padding、borderRadius 保证徽章样式一致。
(3)确认弹窗
React Native 的 Alert.alert 确认弹窗适配为鸿蒙的 AlertDialog 组件,优化医疗数据操作的安全性:
tsx
// React Native - 删除确认
<TouchableOpacity
style={styles.deleteButton}
onPress={() => Alert.alert('删除', `确定要删除这条记录吗?`, [
{ text: '取消' },
{ text: '删除', onPress: () => deleteRecord(item.id) }
])}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
// 鸿蒙
Button('删除')
.backgroundColor('#fee2e2')
.paddingHorizontal(12)
.paddingVertical(6)
.borderRadius(6)
.fontSize(12)
.fontColor('#ef4444')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '删除',
message: '确定要删除这条记录吗?',
confirm: {
value: '删除',
action: () => this.deleteRecord(item.id) // 确认按钮关联操作
},
cancel: { value: '取消' }
});
});
适配优势:
- 类型安全:鸿蒙的弹窗配置有明确的类型定义,避免运行时错误;
- 操作内聚:确认按钮的 action 直接关联删除操作,代码更内聚;
- 用户体验优化:弹窗按钮文字可自定义,更符合中文用户习惯;
- 医疗数据安全:保持删除确认的安全机制,防止误删医疗数据;
- 错误处理完善:可通过 catch 捕获弹窗操作异常,增强应用稳定性。
该血压记录应用的跨端适配实践验证了医疗类应用从 React Native 向鸿蒙迁移的高效性,核心的医疗逻辑和数据模型可实现完全复用,仅需适配UI组件层和交互层。这种适配模式特别适合健康管理类应用开发,能够在保证医疗专业性的前提下,显著提升跨端开发效率,同时利用鸿蒙的原生能力提升应用性能和用户体验。
真实演示案例代码:
js
// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, TextInput } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
health: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
bloodPressure: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
add: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
calendar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
notes: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
chart: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
more: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width, height } = Dimensions.get('window');
// 血压记录类型
type BloodPressureRecord = {
id: string;
systolic: number;
diastolic: number;
heartRate: number;
date: string;
time: string;
notes: string;
status: 'normal' | 'elevated' | 'high' | 'low';
};
const BloodPressureApp: React.FC = () => {
const [records, setRecords] = useState<BloodPressureRecord[]>([
{
id: '1',
systolic: 120,
diastolic: 80,
heartRate: 72,
date: '2023-05-15',
time: '08:30',
notes: '早晨测量,感觉良好',
status: 'normal'
},
{
id: '2',
systolic: 140,
diastolic: 90,
heartRate: 85,
date: '2023-05-14',
time: '14:20',
notes: '下午测量,有些紧张',
status: 'high'
},
{
id: '3',
systolic: 110,
diastolic: 70,
heartRate: 68,
date: '2023-05-13',
time: '20:15',
notes: '晚上测量,放松状态下',
status: 'normal'
}
]);
const [systolic, setSystolic] = useState<string>('');
const [diastolic, setDiastolic] = useState<string>('');
const [heartRate, setHeartRate] = useState<string>('');
const [notes, setNotes] = useState<string>('');
const [isAdding, setIsAdding] = useState<boolean>(false);
// 添加新的血压记录
const addBloodPressureRecord = () => {
if (!systolic || !diastolic || !heartRate) {
Alert.alert('提示', '请填写完整的血压数据');
return;
}
const systolicNum = parseInt(systolic);
const diastolicNum = parseInt(diastolic);
const heartRateNum = parseInt(heartRate);
if (isNaN(systolicNum) || isNaN(diastolicNum) || isNaN(heartRateNum)) {
Alert.alert('提示', '请输入有效的数字');
return;
}
if (systolicNum < 50 || systolicNum > 300 || diastolicNum < 30 || diastolicNum > 200 || heartRateNum < 30 || heartRateNum > 200) {
Alert.alert('提示', '请输入合理的数值范围');
return;
}
const newRecord: BloodPressureRecord = {
id: `${records.length + 1}`,
systolic: systolicNum,
diastolic: diastolicNum,
heartRate: heartRateNum,
date: new Date().toISOString().split('T')[0],
time: new Date().toTimeString().substring(0, 5),
notes: notes || '无备注',
status: getBloodPressureStatus(systolicNum, diastolicNum)
};
setRecords([newRecord, ...records]);
resetForm();
setIsAdding(false);
Alert.alert('成功', '血压记录已保存');
};
// 重置表单
const resetForm = () => {
setSystolic('');
setDiastolic('');
setHeartRate('');
setNotes('');
};
// 根据血压值判断状态
const getBloodPressureStatus = (sys: number, dia: number): 'normal' | 'elevated' | 'high' | 'low' => {
if (sys < 90 || dia < 60) return 'low';
if (sys >= 140 || dia >= 90) return 'high';
if (sys >= 120 && sys <= 139) return 'elevated';
return 'normal';
};
// 获取状态颜色
const getStatusColor = (status: string): string => {
switch (status) {
case 'normal': return '#10b981'; // 绿色
case 'elevated': return '#f59e0b'; // 橙色
case 'high': return '#ef4444'; // 红色
case 'low': return '#3b82f6'; // 蓝色
default: return '#64748b'; // 灰色
}
};
// 渲染血压记录项
const renderRecordItem = ({ item }: { item: BloodPressureRecord }) => (
<View style={styles.recordCard}>
<View style={styles.headerContainer}>
<Text style={styles.dateText}>{item.date}</Text>
<Text style={styles.timeText}>{item.time}</Text>
</View>
<View style={styles.pressureContainer}>
<View style={styles.pressureItem}>
<Text style={styles.pressureValue}>{item.systolic}</Text>
<Text style={styles.pressureLabel}>收缩压</Text>
</div>
<View style={styles.separator}>
<Text>/</Text>
</div>
<View style={styles.pressureItem}>
<Text style={styles.pressureValue}>{item.diastolic}</Text>
<Text style={styles.pressureLabel}>舒张压</Text>
</div>
<View style={styles.hrContainer}>
<Text style={styles.hrValue}>{item.heartRate}</Text>
<Text style={styles.hrLabel}>心率</Text>
</div>
</div>
<View style={styles.statusContainer}>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(item.status) }]}>
<Text style={styles.statusText}>
{item.status === 'normal' ? '正常' :
item.status === 'elevated' ? '偏高' :
item.status === 'high' ? '高血压' : '低血压'}
</Text>
</div>
</div>
<Text style={styles.notesText}>{item.notes}</Text>
<View style={styles.actionContainer}>
<TouchableOpacity
style={styles.editButton}
onPress={() => Alert.alert('编辑', `编辑血压记录 ID: ${item.id}`)}
>
<Text style={styles.editButtonText}>编辑</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => Alert.alert('删除', `确定要删除这条记录吗?`, [
{ text: '取消' },
{ text: '删除', onPress: () => deleteRecord(item.id) }
])}
>
<Text style={styles.deleteButtonText}>删除</Text>
</TouchableOpacity>
</div>
</View>
);
// 删除记录
const deleteRecord = (id: string) => {
setRecords(records.filter(record => record.id !== id));
};
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>血压记录</Text>
<TouchableOpacity
style={styles.addButton}
onPress={() => setIsAdding(!isAdding)}
>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content}>
{/* 搜索栏 */}
<View style={styles.searchContainer}>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={styles.searchPlaceholder}>搜索血压记录</Text>
</div>
{/* 添加血压记录表单 */}
{isAdding && (
<View style={styles.formContainer}>
<Text style={styles.formTitle}>添加血压记录</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>收缩压 (高压) *</Text>
<TextInput
style={styles.input}
value={systolic}
onChangeText={setSystolic}
placeholder="例如: 120"
keyboardType="numeric"
/>
</div>
<View style={styles.inputGroup}>
<Text style={styles.label}>舒张压 (低压) *</Text>
<TextInput
style={styles.input}
value={diastolic}
onChangeText={setDiastolic}
placeholder="例如: 80"
keyboardType="numeric"
/>
</div>
<View style={styles.inputGroup}>
<Text style={styles.label}>心率 *</Text>
<TextInput
style={styles.input}
value={heartRate}
onChangeText={setHeartRate}
placeholder="例如: 72"
keyboardType="numeric"
/>
</div>
<View style={styles.inputGroup}>
<Text style={styles.label}>备注</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={notes}
onChangeText={setNotes}
placeholder="记录时的状态或感受(可选)"
multiline
/>
</div>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setIsAdding(false);
resetForm();
}}
>
<Text style={styles.cancelButtonText}>取消</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.saveButton}
onPress={addBloodPressureRecord}
>
<Text style={styles.saveButtonText}>保存</Text>
</TouchableOpacity>
</div>
</View>
)}
{/* 记录列表标题 */}
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>血压记录</Text>
<Text style={styles.countText}>{records.length} 条记录</Text>
</div>
{/* 血压记录列表 */}
{records.map(record => renderRecordItem({ item: record }))}
{/* 统计信息 */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>
{records.length > 0 ? records[0].systolic : '--'}
</Text>
<Text style={styles.statLabel}>最新收缩压</Text>
</div>
<View style={styles.statItem}>
<Text style={styles.statValue}>
{records.length > 0 ? records[0].diastolic : '--'}
</Text>
<Text style={styles.statLabel}>最新舒张压</Text>
</div>
<View style={styles.statItem}>
<Text style={styles.statValue}>
{records.length > 0 ? records[0].heartRate : '--'}
</Text>
<Text style={styles.statLabel}>最新心率</Text>
</div>
</div>
{/* 使用说明 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>使用说明</Text>
<Text style={styles.infoText}>• 点击+号添加新的血压记录</Text>
<Text style={styles.infoText}>• 正常血压范围: 收缩压 90-140, 舒张压 60-90</Text>
<Text style={styles.infoText}>• 建议每天定时测量血压</Text>
<Text style={styles.infoText}>• 如有异常请及时就医</Text>
</View>
</ScrollView>
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('首页')}
>
<Text style={styles.navIcon}>🏠</Text>
<Text style={styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('血压')}
>
<Text style={styles.navIcon}>🩸</Text>
<Text style={styles.navText}>血压</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('图表')}
>
<Text style={styles.navIcon}>📊</Text>
<Text style={styles.navText}>图表</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('我的')}
>
<Text style={styles.navIcon}>👤</Text>
<Text style={styles.navText}>我的</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
},
addButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#3b82f6',
alignItems: 'center',
justifyContent: 'center',
},
addButtonText: {
fontSize: 20,
color: '#ffffff',
fontWeight: 'bold',
},
content: {
flex: 1,
padding: 16,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#ffffff',
borderRadius: 20,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
searchIcon: {
fontSize: 18,
color: '#64748b',
},
searchPlaceholder: {
fontSize: 14,
color: '#94a3b8',
marginLeft: 12,
flex: 1,
},
formContainer: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
formTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 16,
},
inputGroup: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
color: '#1e293b',
marginBottom: 8,
},
input: {
backgroundColor: '#f1f5f9',
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 12,
fontSize: 16,
color: '#1e293b',
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
buttonGroup: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16,
},
cancelButton: {
flex: 1,
backgroundColor: '#f1f5f9',
padding: 12,
borderRadius: 8,
alignItems: 'center',
marginRight: 8,
},
cancelButtonText: {
fontSize: 16,
color: '#64748b',
fontWeight: '500',
},
saveButton: {
flex: 1,
backgroundColor: '#3b82f6',
padding: 12,
borderRadius: 8,
alignItems: 'center',
marginLeft: 8,
},
saveButtonText: {
fontSize: 16,
color: '#ffffff',
fontWeight: '500',
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
},
countText: {
fontSize: 14,
color: '#64748b',
},
recordCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
dateText: {
fontSize: 14,
color: '#64748b',
fontWeight: '500',
},
timeText: {
fontSize: 14,
color: '#64748b',
},
pressureContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
pressureItem: {
alignItems: 'center',
flex: 1,
},
separator: {
alignItems: 'center',
justifyContent: 'center',
width: 20,
},
hrContainer: {
alignItems: 'center',
flex: 1,
},
pressureValue: {
fontSize: 24,
fontWeight: 'bold',
color: '#1e293b',
},
pressureLabel: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
},
hrValue: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
},
hrLabel: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
},
statusContainer: {
alignItems: 'flex-start',
marginBottom: 12,
},
statusBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
color: '#ffffff',
fontWeight: '500',
},
notesText: {
fontSize: 14,
color: '#64748b',
marginBottom: 12,
},
actionContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
editButton: {
backgroundColor: '#f1f5f9',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
marginRight: 8,
},
editButtonText: {
fontSize: 12,
color: '#3b82f6',
fontWeight: '500',
},
deleteButton: {
backgroundColor: '#fee2e2',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
deleteButtonText: {
fontSize: 12,
color: '#ef4444',
fontWeight: '500',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
statItem: {
alignItems: 'center',
},
statValue: {
fontSize: 18,
fontWeight: 'bold',
color: '#3b82f6',
},
statLabel: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
textAlign: 'center',
},
infoCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginTop: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
infoTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
infoText: {
fontSize: 14,
color: '#64748b',
lineHeight: 22,
marginBottom: 8,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
});
export default BloodPressureApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文分析了基于React Native的血压监测应用实现,重点探讨了其架构设计、状态管理和跨端兼容性策略。应用采用单组件架构,包含主应用组件、记录列表、添加表单等模块,通过useState管理状态并实现数据验证。文章详细介绍了血压状态自动判断逻辑、TypeScript类型定义以及视觉交互设计。同时提出了性能优化方案,如使用FlatList替代ScrollView,并讨论了React Native与鸿蒙系统的跨端兼容性考虑。该实现提供了清晰的血压记录管理功能,具有良好的用户体验和可维护性。