React Native鸿蒙跨平台医疗健康类的血压记录,包括收缩压、舒张压、心率、日期、时间、备注和状态

在移动应用开发中,健康监测类应用是一种重要的应用类型,需要考虑数据记录、状态判断、用户交互等多个方面。本文将深入分析一个功能完备的 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 与鸿蒙跨端考虑

在设计跨端血压监测应用时,需要特别关注以下几个方面:

  1. 组件 API 兼容性 - 确保使用的 React Native 组件在鸿蒙系统上有对应实现
  2. 样式系统差异 - 不同平台对样式的支持程度不同,需要确保样式在两端都能正常显示
  3. 触摸事件处理 - 不同平台的触摸事件机制可能存在差异
  4. 图标系统 - 确保图标在不同平台上都能正常显示
  5. 数据处理 - 不同平台的数据处理性能可能存在差异

当前实现使用 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与鸿蒙系统的跨端兼容性考虑。该实现提供了清晰的血压记录管理功能,具有良好的用户体验和可维护性。

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

相关推荐
落霞的思绪2 小时前
配置React和React-dom为CDN引入
前端·react.js·前端框架
橙露3 小时前
React Hooks 深度解析:从基础使用到自定义 Hooks 的封装技巧
javascript·react.js·ecmascript
2501_920931703 小时前
React Native鸿蒙跨平台使用useState管理健康记录和过滤状态,支持多种健康数据类型(血压、体重等)并实现按类型过滤功能
javascript·react native·react.js·ecmascript·harmonyos
打小就很皮...3 小时前
dnd-kit 实现表格拖拽排序
前端·react.js·表格拖拽·dnd-kit
Ulyanov3 小时前
从静态到沉浸:打造惊艳的Web技术发展历程3D时间轴
前端·javascript·html5·gui开发
2501_921930833 小时前
高级进阶 React Native 鸿蒙跨平台开发:InteractionManager 交互优化
react native·harmonyos
打小就很皮...3 小时前
React 19 + Vite 6 + SWC 构建优化实践
前端·react.js·vite·swc
Highcharts.js3 小时前
使用Highcharts与React集成 官网文档使用说明
前端·react.js·前端框架·react·highcharts·官方文档
前端不太难3 小时前
HarmonyOS PC 文档模型完整范式
华为·状态模式·harmonyos