ReactNative 鸿蒙跨端行业统称RNOH(React Native for OpenHarmony),是社区主导、华为协同共建的适配兼容层,核心是给原生 React Native 框架补上鸿蒙平台的底层实现,让一套 React/TypeScript 业务代码,无需大规模改写、不用学习 ArkTS 原生语法,就能同时编译运行在安卓、iOS、鸿蒙手机、平板、智慧屏、PC 全场景设备,最终打包为鸿蒙标准 HAP 安装包直接上架华为应用市场,它属于原生控件渲染型跨端,和 Uniapp、Cordova 这类 WebView 网页套壳跨端有着本质区别,页面渲染、手势交互、硬件调用全部依托鸿蒙 ArkUI 原生图形引擎完成,不存在浏览器内核带来的性能损耗。

底层运行架构原理

整套架构基于 React Native新架构(Fabric 渲染器 + JSI 直通通信 + TurboModule 按需模块)搭建,摒弃旧版 RN 异步 JSON 桥接的低效模式,分为三层执行链路。最上层是开发者编写的 React 业务代码,通过 Metro 打包后依托 Hermes JS 引擎执行逻辑、生成虚拟 DOM 节点;中间层是 RNOH 核心适配层,依靠 JSI 共享内存直接打通 JS 层与 C++ 原生层,用 Yoga 引擎统一完成 Flex 布局计算,Fabric 调度渲染任务,TurboModule 实现系统能力懒加载,避免 App 启动全量初始化拖慢开机速度;最底层直接通过鸿蒙 ArkUI C-API 对接系统原生组件,RN 里的 View、Text、ScrollView、Image、点击组件会一一映射为鸿蒙 Div、Text、Scroll、Image、Button 原生控件,弹窗、状态栏、权限、传感器、文件存储等系统调用,同样由适配层转发给鸿蒙系统 NAPI 接口实现调用,同时额外封装了鸿蒙分布式软总线、原子化服务、折叠屏自适应、多设备流转等独有系统接口,供 RN 业务直接调用。

开发与工程使用形态

前端开发者维持原有 RN 开发习惯,使用 React Hooks、React Navigation 路由、Redux/Zustand 状态管理、Axios 网络请求等成熟前端工具链,业务代码里仅需少量平台判断处理差异化逻辑,绝大部分业务代码和安卓 iOS 两端完全复用。工程构建上,在原有 RN 项目中接入 RNOH 鸿蒙工程模板,通过鸿蒙 DevEco 工具链编译,直接产出 HAP/HSP 鸿蒙安装包,支持混合原生开发,遇到 RN 生态缺失的硬件、底层系统能力时,可以直接用 ArkTS/C++ 编写鸿蒙原生 TurboModule 模块,导出 JS 接口给 React 业务调用,补齐能力短板。

该药品管理应用采用了清晰的模块化架构,通过 TypeScript 接口定义了三个核心数据模型:Medicine(药品信息)、MedicationRecord(服药记录)和 Location(存放位置)。这种强类型设计为跨端开发奠定了坚实基础,确保了数据结构在不同平台上的一致性。

在 React Native 跨端到 HarmonyOS 的场景中,TypeScript 类型系统提供了良好的兼容性保障。Medicine 类型包含了药品的核心属性,如名称、剂量、服用频率、剩余数量等,这种结构化数据设计便于在不同平台间进行序列化和反序列化。Location 类型则建立了药品与存放位置的关联关系,通过 medicineIds 数组实现了一对多的数据映射,这在跨端数据库设计中是常见的最佳实践。

状态管理

应用使用 React Hooks 中的 useState 管理三个核心数据源:medicineslocationsmedicationRecords。这种函数式状态管理方式在 React Native 和 HarmonyOS ArkUI 中都得到了很好的支持,是跨端开发的推荐方案。

在 HarmonyOS 跨端适配中,useState Hook 可以直接映射到 ArkUI 的 @State 装饰器,实现状态的响应式更新。值得注意的是,当状态数据量较大时,建议使用 useMemouseCallback 进行性能优化,避免不必要的组件重渲染。应用中的 getMedicinesForLocation 函数通过过滤算法实现了药品与位置的关联查询,这种计算逻辑在跨端开发中应考虑缓存机制,以提升应用性能。

UI 组件

应用使用了 React Native 核心组件库构建用户界面,主要包括:

  • SafeAreaView 确保内容在安全区域内显示,适配不同设备的屏幕特性
  • FlatList 高效渲染药品列表,支持长列表的性能优化
  • TouchableOpacity 实现可点击的药品卡片,提供视觉反馈
  • StyleSheet 实现样式与逻辑分离,便于维护和主题切换

在 HarmonyOS 跨端开发中,这些组件可以映射到 ArkUI 对应的组件:

  • FlatList 对应 ArkUI 的 List 组件,同样支持虚拟列表优化
  • TouchableOpacity 对应 ArkUI 的 Button 组件或 Gesture 手势组件
  • StyleSheet 的样式定义可以通过工具转换为 ArkUI 的样式系统

需要特别注意的是,React Native 的 Flexbox 布局在 HarmonyOS 上有很好的兼容性,但部分样式属性(如阴影效果、圆角处理)可能需要针对不同平台进行微调。应用中使用 Dimensions.get('window') 获取屏幕尺寸,在 HarmonyOS 中可以通过 window.getWindowProperties API 实现类似功能,建议封装统一的工具函数以处理平台差异。


应用通过 TouchableOpacity 实现了药品卡片的点击交互,并使用 Alert 组件显示药品详情。这种简洁的交互设计符合移动端应用的设计规范,在跨端开发中需要注意:

  1. Alert.alert 在 HarmonyOS 中对应 dialog.showAlertDialog API,需要进行适配
  2. 触摸反馈效果在不同平台上的表现可能存在差异,建议使用统一的反馈机制
  3. 药品卡片的布局设计考虑了响应式需求,通过 StyleSheet 中的 width 计算实现了适配不同屏幕尺寸的效果

应用中的 medicineCard 组件采用了左右布局,左侧显示药品图标,右侧显示详细信息,这种布局方式在 React Native 和 HarmonyOS 上都能获得良好的显示效果。药品状态通过 remaining 属性直观展示,便于用户了解药品库存情况,这种设计思路值得在跨端应用中推广。

列表渲染

应用在列表渲染中使用了 FlatList 组件,这是 React Native 处理长列表的最佳实践。FlatList 实现了虚拟列表技术,只渲染可见区域内的组件,大幅提升了长列表的性能。在 HarmonyOS 跨端开发中,FlatList 同样能获得良好的性能表现,但需要注意:

  1. 确保为每个列表项提供唯一的 key 属性,避免不必要的组件重渲染
  2. 复杂列表项建议使用 memouseMemo 进行优化
  3. 图片资源(如示例中的 base64 图标)应合理管理,在生产环境中建议使用图片文件而非 base64 编码,以减少内存占用

应用中的 getMedicinesForLocation 函数通过过滤算法实现了数据关联查询,在数据量较大时可能成为性能瓶颈。建议在跨端应用中考虑使用更高效的数据结构(如 Map)或引入状态管理库(如 Redux、MobX)来优化数据查询和状态更新。

  1. 组件拆分与复用:当前应用将所有逻辑集中在一个组件中,建议将药品卡片、位置列表等功能拆分为独立的子组件,提高代码复用性和可维护性。在跨端开发中,组件化设计尤为重要,便于在不同平台上进行差异化适配。

  2. API 封装与平台适配:应用中直接使用了 AlertDimensions 等 React Native API,建议将这些 API 封装为统一的工具函数,处理不同平台的差异。例如:

// 跨端弹窗工具函数
export const showAlert = (title: string, message: string) => {
  // 在不同平台上调用不同的 API
  if (Platform.OS === 'harmony') {
    // HarmonyOS 实现
  } else {
    // React Native 实现
    Alert.alert(title, message);
  }
};
  1. 样式主题化:当前应用的样式直接定义在 StyleSheet 中,建议提取样式变量为主题常量,便于统一管理和主题切换。在跨端应用中,主题化设计可以提高应用的可定制性,适应不同平台的设计规范。

  2. 数据持久化:应用中的数据当前存储在内存中,建议引入本地存储方案(如 AsyncStorage、SQLite)实现数据持久化。在 HarmonyOS 跨端开发中,可以使用 @ohos.data.storage@ohos.data.relationalStore 实现类似功能。

  3. 错误处理与边界情况:应用中缺少完善的错误处理机制,建议添加 try-catch 块捕获可能的异常,特别是在数据处理和 API 调用过程中。在跨端应用中,良好的错误处理可以提高应用的稳定性和用户体验。

该药品管理应用展示了 React Native 跨端开发的核心技术要点,包括数据模型设计、状态管理、UI 组件适配和性能优化等。通过深入理解这些技术点,并结合 HarmonyOS 的特性进行适配,可以打造出既符合 React Native 开发规范,又能充分利用 HarmonyOS 平台优势的跨端应用。


在家庭医疗健康类应用的迭代演进中,药品“位置维度”的管理成为提升用户体验的核心诉求——从单纯的药品信息管理,延伸到“药品-存放位置”的关联管理,既符合家庭用药的实际场景,也为跨端开发提出了“关联数据建模、嵌套视图渲染、跨端体验一致性”的新挑战。本文以 React Native 开发的药品位置管理应用为例,深度拆解其“位置-药品”双层关联数据模型设计、嵌套列表渲染逻辑,并系统阐述向鸿蒙(HarmonyOS)ArkTS 跨端迁移的技术路径,聚焦“关联数据分层建模、位置维度数据聚合、跨端嵌套视图等价实现”三大核心维度,为医疗健康类应用的跨端迭代提供可落地的技术参考。

数据模型

该应用在原有药品数据模型基础上,新增 Location 类型构建“位置-药品”的双向关联体系,是跨端开发中“关联数据层复用”的核心基础,完全贴合家庭用药的实际场景:

  • 扩展后的 Medicine 类型新增 location 字段,直接关联具体存放位置名称(如“主卧床头柜”),作为药品的核心属性之一,同时保留原有医疗场景字段(category/dosage/frequency 等),保证医疗数据的完整性;
  • 新增 Location 类型定义存放位置核心属性:id 作为唯一标识,name/description 描述位置信息,medicineIds 数组存储关联药品 ID,icon 字段通过 emoji 符号(🛏️/🛋️/🍽️)实现位置可视化,形成“位置包含药品”的关联关系;
  • 数据关联逻辑通过 getMedicinesForLocation 函数实现:通过 location.id 找到对应位置,再通过 medicineIds 筛选关联药品,完成“位置→药品”的反向查询,该函数是关联数据场景的核心复用单元。

这种“药品归属位置、位置包含药品”的双向关联模型,既符合家庭用药“按位置存放、按位置查找”的实际场景,又通过 TypeScript 强类型约束保证数据关联的准确性——所有类型定义可直接迁移至鸿蒙 ArkTS 工程,无需任何修改,保证了跨端关联数据层的 100% 复用。

业务逻辑

应用将“位置维度”的核心业务逻辑封装为纯函数,与 UI 渲染层完全解耦,为跨端迁移提供了“逻辑复用、视图重构”的清晰边界,典型场景包括:

  • getMedicinesForLocation 函数实现“位置→药品”的关联查询:先通过 locationId 定位位置实体,再通过 medicineIds 数组筛选关联药品,该函数是医疗场景中“一对多”关联数据查询的典型实现,可直接复用于鸿蒙端;
  • 位置药品数量统计:通过 locationMedicines.length 实时计算每个位置的药品数量,为位置卡片展示提供核心数据,符合家庭用药“按位置盘点药品”的实际需求;
  • 药品分类统计:延续原有医疗场景逻辑,通过 medicines.filter(m => m.category === '处方药') 实现不同药品类型的数量统计,保证医疗数据统计维度的一致性;
  • 条件渲染逻辑:在“各位置药品明细”模块中,通过 if (locationMedicines.length === 0) return null 过滤无药品的位置,避免空视图展示,提升用户体验。

所有关联数据处理逻辑均为纯函数,无 UI 层依赖,这是跨端开发中“关联逻辑复用”的核心——鸿蒙端只需复用这些函数,无需重新开发“位置-药品”关联的核心业务逻辑。

嵌套视图

应用在原有组件化架构基础上,新增“位置列表+位置下药品明细”的嵌套视图结构,所有模块均围绕“位置维度管理药品”的核心诉求定制化实现:

头部与统计卡片

头部区域延续原有布局逻辑,标题更新为“药箱位置管理”,添加按钮绑定“添加位置”弹窗,贴合“位置管理”的核心场景;统计卡片新增“存放位置”统计项,与“药品数量”“即将到期”形成三大核心指标,通过 statNumber(大号粗体蓝色)与 statLabel(小号浅灰色)的视觉层级差异,突出“位置数量”这一新核心维度,满足用户快速获取位置与药品整体概况的需求。

位置列表

存放位置列表是应用的核心模块,通过 FlatList 实现高性能渲染,核心亮点在于“位置+关联药品数量”的聚合展示:

  • 列表项 renderLocationItem 整合位置基础信息(名称、描述)、可视化图标(emoji)、关联药品数量,通过 getMedicinesForLocation(item.id) 实时计算该位置的药品数量,形成“位置→药品数量”的动态关联;
  • 位置卡片采用“图标+信息+箭头”的三栏布局:左侧图标区域通过 borderRadius: 8 实现圆角背景,中间信息区域展示位置核心信息,右侧箭头引导进入位置详情,符合移动端“列表项→详情页”的交互习惯;
  • 关联药品数量采用浅灰色小号字体(#94a3b8/12号),既保证信息可见性,又不抢夺位置名称的视觉焦点,符合医疗应用“核心信息优先”的设计原则。

FlatListshowsVerticalScrollIndicator={false} 配置隐藏滚动条,保证医疗应用界面的简洁性,同时通过 keyExtractor={item => item.id} 保证列表项的唯一性,避免渲染异常。

药品明细

“各位置药品明细”模块是跨端开发中“嵌套视图”的典型场景,核心实现逻辑为:

  • 外层通过 locations.map 遍历所有位置,内层通过 locationMedicines.map 遍历该位置下的药品,形成“位置分组→药品列表”的嵌套结构;
  • 通过 if (locationMedicines.length === 0) return null 过滤无药品的位置,避免空分组展示,提升界面整洁性;
  • 位置分组头部采用“图标+名称”的横向布局,背景色采用浅灰色(#f8fafc)与药品卡片的白色(#ffffff)形成视觉区分,符合嵌套视图的层级设计规范;
  • 药品迷你卡片(medicineMiniCard)采用更小的尺寸和内边距,保留核心信息(名称、剂量、剩余数量),通过 elevation/shadow 实现轻微悬浮效果,与位置分组背景形成层次感,符合医疗应用“信息分层展示”的设计原则。

该嵌套视图结构完全贴合家庭用药“按位置查找药品”的实际场景,是跨端适配中“视图语义等价”的核心挑战点。

分类统计

药品分类统计模块延续原有布局逻辑,通过横向三等分布局展示“处方药/非处方药/保健品”的数量统计,图标采用医疗场景专属符号(℞/💊/🌿),数量统计采用大号粗体蓝色,保证医疗数据统计维度的一致性,同时与“位置维度”形成互补,满足用户从不同维度查看药品的需求。

底部导航

底部导航在原有“首页/药箱/我的”基础上,新增“位置”导航项,图标采用📍符号,与“位置管理”核心场景匹配;选中项通过顶部蓝色边框(borderTopColor: #3b82f6)形成清晰的选中反馈,保证用户操作路径的清晰性,符合医疗应用“核心场景快速访问”的设计原则。

样式

应用在原有样式体系基础上,新增“位置相关样式”“嵌套视图样式”,形成“通用样式+医疗场景样式+位置管理样式+状态样式”四层样式体系,是跨端适配中“视觉一致性”的核心保障:

  • 通用样式:所有卡片组件统一使用 borderRadius: 12、跨平台阴影(elevation 适配 Android,shadow 系列属性适配 iOS),保证医疗应用界面的专业感与统一性;
  • 医疗场景样式:延续原有药品分类标签(backgroundColor: #ecfdf5 + color: #10b981)、药品核心信息样式,保证医疗数据展示的一致性;
  • 位置管理样式:新增 locationCard/locationIcon/locationMedicinesSection 等样式,定义位置卡片、位置分组、迷你药品卡片的视觉规范,通过 #f8fafc 浅灰色背景区分位置分组与药品卡片,形成嵌套视图的视觉层级;
  • 状态样式:底部导航选中态、位置箭头图标样式,均通过样式的条件渲染实现状态区分,保证位置管理场景的状态展示清晰性。

所有新增样式数值(如位置卡片内边距、迷你药品卡片圆角)均与原有样式保持统一规范,是跨端适配中“视觉像素级一致”的基础——鸿蒙端只需复用这些数值,即可保证医疗应用视觉效果的统一。


核心技术体系的等价映射

跨端适配的核心是“关联数据层复用、关联逻辑层复用、嵌套视图层等价重构”,React Native 与鸿蒙 ArkTS 的核心能力可实现精准映射,以下是位置管理场景关键技术点的等价转换:

React Native 核心能力 鸿蒙 ArkTS 等价实现 位置管理场景适配要点
TypeScript 类型定义(含 Location) TypeScript 类型定义(含 Location) Medicine/Location/MedicationRecord 类型 100% 复用,保证“位置-药品”关联数据结构的一致性
useState 状态管理 @State 装饰器 药品/位置初始数据完全复用,medicineIds 数组关联逻辑保持不变
getMedicinesForLocation 关联查询函数 同名类方法 100% 复用关联查询逻辑,仅需调整 this 指向,保证“位置→药品”查询结果一致
FlatList 位置列表渲染 List + ListItem datalistDatarenderItemitemGenerator,适配位置列表的长列表场景
嵌套 map 渲染(locations.map + locationMedicines.map) 嵌套 ForEach 外层 ForEach 遍历位置,内层 ForEach 遍历药品,保持嵌套视图结构一致
TouchableOpacity 位置卡片交互 Button 组件 onPressonClick,设置 backgroundColor: Transparent 去除默认样式,保证位置卡片交互体验一致
条件渲染(locationMedicines.length === 0) 条件渲染(if 语句) 完全复用空位置过滤逻辑,避免鸿蒙端空视图展示
嵌套视图样式(locationMedicinesSection) 嵌套容器样式 ViewColumn/Row,样式数值 100% 复用,保证嵌套视图视觉层级一致
关联数据层

React Native 端的 useState 状态定义可直接转换为鸿蒙的 @State 装饰器,“位置-药品”关联业务逻辑函数完全复用:

// React Native 端
const [medicines, setMedicines] = useState<Medicine[]>([/* 初始药品数据 */]);
const [locations, setLocations] = useState<Location[]>([/* 初始位置数据 */]);

// 鸿蒙 ArkTS 端
@State medicines: Medicine[] = [/* 初始药品数据,100% 复用 */];
@State locations: Location[] = [/* 初始位置数据,100% 复用 */];

// 关联查询函数 100% 复用(仅调整 this 指向)
getMedicinesForLocation(locationId: string): Medicine[] {
  const location = this.locations.find(loc => loc.id === locationId);
  if (!location) return [];
  
  return this.medicines.filter(med => location.medicineIds.includes(med.id));
}

需要注意的是,鸿蒙端的状态更新无需调用 setLocations 等函数,直接赋值即可(如 this.locations = newLocations),但“位置-药品”关联的核心逻辑(medicineIds 数组匹配)完全复用 React Native 端的实现。

位置列表(FlatList → List)迁移

React Native 的 FlatList 是位置列表渲染的核心组件,鸿蒙端通过 List 组件实现等价效果,“位置→药品数量”的关联计算逻辑完全复用:

// 鸿蒙 ArkTS 位置列表实现
@Builder
renderLocationList() {
  Column() {
    // 列表标题栏
    Text('药品存放位置')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .color('#1e293b')
      .padding(16)
      .backgroundColor('#ffffff')
      .borderBottom({ width: 1, color: '#e2e8f0' });
    
    // 列表渲染
    List() {
      ForEach(this.locations, (item: Location) => {
        ListItem() {
          this.renderLocationItem(item); // 复用位置项渲染逻辑
        }
      }, item => item.id); // 对应 keyExtractor
    }
    .showsScrollbar(false) // 对应 showsVerticalScrollIndicator={false}
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .marginBottom(16)
    .shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
  }
}

// 位置项渲染函数(关联数量计算逻辑复用)
@Builder
renderLocationItem(item: Location) {
  // 核心:位置关联药品数量计算(100% 复用 React Native 逻辑)
  const locationMedicines = this.getMedicinesForLocation(item.id);
  
  Row() {
    // 位置图标(等价布局)
    Column()
      .width(50)
      .height(50)
      .borderRadius(8)
      .backgroundColor('#e2e8f0')
      .justifyContent(FlexAlign.Center)
      .alignItems(ItemAlign.Center)
      .marginRight(12)
    {
      Text(item.icon).fontSize(24);
    }
    
    // 位置信息区域(关联数据完全复用)
    Column().flexGrow(1) {
      Text(item.name)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .color('#1e293b')
        .marginBottom(4);
      
      Text(item.description)
        .fontSize(14)
        .color('#64748b')
        .marginBottom(4);
      
      Text(`${locationMedicines.length} 种药品`)
        .fontSize(12)
        .color('#94a3b8');
    }
    
    // 位置箭头
    Column()
      .justifyContent(FlexAlign.Center)
    {
      Text('›')
        .fontSize(16)
        .color('#94a3b8');
    }
  }
  .padding(16)
  .borderBottom({ width: 1, color: '#e2e8f0' });
}

可以看到,除了组件语法(如 Row/Column 替代 View)和样式写法的调整,核心的位置管理逻辑——位置关联药品数量计算、位置信息展示、箭头引导交互——完全复用 React Native 端的实现,保证了跨端位置列表展示的一致性。


真实演示案例代码:

// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList } from 'react-native';

// Base64 图标库
const ICONS_BASE64 = {
  medicine: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  pill: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  location: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  reminder: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  profile: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  settings: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  add: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
  search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};

const { width, height } = Dimensions.get('window');

// 药品类型
type Medicine = {
  id: string;
  name: string;
  dosage: string; // 剂量
  frequency: string; // 服用频率
  startDate: string;
  endDate: string;
  instructions: string;
  category: '处方药' | '非处方药' | '保健品';
  remaining: number; // 剩余数量
  location: string; // 存放位置
};

// 服药记录类型
type MedicationRecord = {
  id: string;
  medicineId: string;
  date: string;
  time: string;
  doseTaken: number;
  notes: string;
  taken: boolean; // 是否已服用
};

// 存放位置类型
type Location = {
  id: string;
  name: string;
  description: string;
  medicineIds: string[];
  icon: string;
};

const MedicineLocationApp: React.FC = () => {
  const [medicines, setMedicines] = useState<Medicine[]>([
    { 
      id: '1', 
      name: '阿莫西林胶囊', 
      dosage: '0.25g*2粒', 
      frequency: '每日3次', 
      startDate: '2023-06-01', 
      endDate: '2023-06-10', 
      instructions: '饭后服用,温水送服', 
      category: '处方药', 
      remaining: 20,
      location: '主卧床头柜'
    },
    { 
      id: '2', 
      name: '维生素C片', 
      dosage: '0.1g*1片', 
      frequency: '每日1次', 
      startDate: '2023-05-20', 
      endDate: '2023-07-20', 
      instructions: '早饭后服用', 
      category: '非处方药', 
      remaining: 50,
      location: '客厅茶几抽屉'
    },
    { 
      id: '3', 
      name: '复方甘草片', 
      dosage: '2片', 
      frequency: '每日3次', 
      startDate: '2023-06-05', 
      endDate: '2023-06-15', 
      instructions: '咳嗽时服用', 
      category: '非处方药', 
      remaining: 15,
      location: '主卧床头柜'
    },
    { 
      id: '4', 
      name: '钙尔奇D', 
      dosage: '600mg*1片', 
      frequency: '每日1次', 
      startDate: '2023-04-01', 
      endDate: '2023-10-01', 
      instructions: '晚饭后服用', 
      category: '保健品', 
      remaining: 80,
      location: '厨房食品柜'
    },
    { 
      id: '5', 
      name: '布洛芬缓释胶囊', 
      dosage: '0.3g*1粒', 
      frequency: '必要时服用', 
      startDate: '2023-05-01', 
      endDate: '2024-05-01', 
      instructions: '饭后服用,止痛用', 
      category: '非处方药', 
      remaining: 12,
      location: '卫生间镜柜'
    },
  ]);

  const [locations, setLocations] = useState<Location[]>([
    { id: '1', name: '主卧床头柜', description: '卧室常用药品存放处', medicineIds: ['1', '3'], icon: '🛏️' },
    { id: '2', name: '客厅茶几抽屉', description: '客厅临时用药存放', medicineIds: ['2'], icon: '🛋️' },
    { id: '3', name: '厨房食品柜', description: '保健品存放处', medicineIds: ['4'], icon: '🍽️' },
    { id: '4', name: '卫生间镜柜', description: '急救药品存放处', medicineIds: ['5'], icon: '🚽' },
    { id: '5', name: '书房抽屉', description: '备用药品存放', medicineIds: [], icon: '📚' },
  ]);

  const [medicationRecords] = useState<MedicationRecord[]>([
    { id: '1', medicineId: '1', date: '2023-06-05', time: '08:00', doseTaken: 2, notes: '按时服用', taken: true },
    { id: '2', medicineId: '1', date: '2023-06-05', time: '14:00', doseTaken: 2, notes: '按时服用', taken: true },
    { id: '3', medicineId: '1', date: '2023-06-05', time: '20:00', doseTaken: 2, notes: '按时服用', taken: true },
    { id: '4', medicineId: '2', date: '2023-06-05', time: '09:00', doseTaken: 1, notes: '按时服用', taken: true },
  ]);

  // 获取特定位置的药品
  const getMedicinesForLocation = (locationId: string) => {
    const location = locations.find(loc => loc.id === locationId);
    if (!location) return [];
    
    return medicines.filter(med => location.medicineIds.includes(med.id));
  };

  // 渲染药品项
  const renderMedicineItem = ({ item }: { item: Medicine }) => {
    const location = locations.find(loc => loc.name === item.location);
    
    return (
      <TouchableOpacity 
        style={styles.medicineCard}
        onPress={() => Alert.alert('药品详情', `查看 ${item.name} 的详细信息`)}
      >
        <View style={styles.medicineIcon}>
          <Text style={styles.medicineIconText}>💊</Text>
        </View>
        <View style={styles.medicineInfo}>
          <Text style={styles.medicineName}>{item.name}</Text>
          <Text style={styles.medicineDosage}>{item.dosage}</Text>
          <Text style={styles.medicineFrequency}>{item.frequency}</Text>
          <View style={styles.medicineMeta}>
            <Text style={styles.medicineCategory}>{item.category}</Text>
            <Text style={styles.medicineRemaining}>剩余: {item.remaining}</Text>
          </View>
        </View>
        <View style={styles.medicineLocation}>
          <Text style={styles.locationText}>{location?.icon} {item.location}</Text>
        </View>
      </TouchableOpacity>
    );
  };

  // 渲染位置项
  const renderLocationItem = ({ item }: { item: Location }) => {
    const locationMedicines = getMedicinesForLocation(item.id);
    
    return (
      <TouchableOpacity 
        style={styles.locationCard}
        onPress={() => Alert.alert('位置详情', `查看 ${item.name} 中的药品`)}
      >
        <View style={styles.locationIcon}>
          <Text style={styles.locationIconText}>{item.icon}</Text>
        </View>
        <View style={styles.locationInfo}>
          <Text style={styles.locationName}>{item.name}</Text>
          <Text style={styles.locationDescription}>{item.description}</Text>
          <Text style={styles.locationMedicineCount}>{locationMedicines.length} 种药品</Text>
        </View>
        <View style={styles.locationAction}>
          <Text style={styles.locationArrow}></Text>
        </View>
      </TouchableOpacity>
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      {/* 头部 */}
      <View style={styles.header}>
        <Text style={styles.title}>药箱位置管理</Text>
        <TouchableOpacity 
          style={styles.addButton}
          onPress={() => Alert.alert('添加位置', '添加新的药品存放位置')}
        >
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>

      <ScrollView style={styles.content}>
        {/* 统计卡片 */}
        <View style={styles.statsCard}>
          <View style={styles.statItem}>
            <Text style={styles.statNumber}>{medicines.length}</Text>
            <Text style={styles.statLabel}>药品数量</Text>
          </View>
          <View style={styles.statItem}>
            <Text style={styles.statNumber}>{locations.length}</Text>
            <Text style={styles.statLabel}>存放位置</Text>
          </View>
          <View style={styles.statItem}>
            <Text style={styles.statNumber}>3</Text>
            <Text style={styles.statLabel}>即将到期</Text>
          </View>
        </View>

        {/* 存放位置列表 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>药品存放位置</Text>
          <FlatList
            data={locations}
            renderItem={renderLocationItem}
            keyExtractor={item => item.id}
            showsVerticalScrollIndicator={false}
          />
        </View>

        {/* 各位置药品明细 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>各位置药品明细</Text>
          {locations.map(location => {
            const locationMedicines = getMedicinesForLocation(location.id);
            if (locationMedicines.length === 0) return null;
            
            return (
              <View key={location.id} style={styles.locationMedicinesSection}>
                <View style={styles.locationHeader}>
                  <Text style={styles.locationHeaderIcon}>{location.icon}</Text>
                  <Text style={styles.locationHeaderText}>{location.name}</Text>
                </View>
                {locationMedicines.map(medicine => (
                  <TouchableOpacity 
                    key={medicine.id}
                    style={styles.medicineMiniCard}
                    onPress={() => Alert.alert('药品详情', `查看 ${medicine.name} 的详细信息`)}
                  >
                    <Text style={styles.medicineMiniName}>{medicine.name}</Text>
                    <Text style={styles.medicineMiniDosage}>{medicine.dosage}</Text>
                    <Text style={styles.medicineMiniRemaining}>剩余: {medicine.remaining}</Text>
                  </TouchableOpacity>
                ))}
              </View>
            );
          })}
        </View>

        {/* 药品分类统计 */}
        <View style={styles.categoryStatsCard}>
          <Text style={styles.categoryStatsTitle}>药品分类统计</Text>
          <View style={styles.categoryStatsItems}>
            <View style={styles.categoryStatItem}>
              <Text style={styles.categoryStatIcon}></Text>
              <Text style={styles.categoryStatText}>处方药</Text>
              <Text style={styles.categoryStatNumber}>{medicines.filter(m => m.category === '处方药').length}</Text>
            </View>
            <View style={styles.categoryStatItem}>
              <Text style={styles.categoryStatIcon}>💊</Text>
              <Text style={styles.categoryStatText}>非处方药</Text>
              <Text style={styles.categoryStatNumber}>{medicines.filter(m => m.category === '非处方药').length}</Text>
            </View>
            <View style={styles.categoryStatItem}>
              <Text style={styles.categoryStatIcon}>🌿</Text>
              <Text style={styles.categoryStatText}>保健品</Text>
              <Text style={styles.categoryStatNumber}>{medicines.filter(m => m.category === '保健品').length}</Text>
            </View>
          </View>
        </View>

        {/* 位置管理功能 */}
        <View style={styles.managementCard}>
          <Text style={styles.managementTitle}>位置管理</Text>
          <View style={styles.managementItems}>
            <TouchableOpacity 
              style={styles.managementItem}
              onPress={() => Alert.alert('添加位置', '添加新的药品存放位置')}
            >
              <Text style={styles.managementIcon}></Text>
              <Text style={styles.managementText}>添加位置</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={styles.managementItem}
              onPress={() => Alert.alert('重新分配', '重新分配药品到不同位置')}
            >
              <Text style={styles.managementIcon}>🔄</Text>
              <Text style={styles.managementText}>重新分配</Text>
            </TouchableOpacity>
            <TouchableOpacity 
              style={styles.managementItem}
              onPress={() => Alert.alert('整理建议', '获取药品整理建议')}
            >
              <Text style={styles.managementIcon}>💡</Text>
              <Text style={styles.managementText}>整理建议</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 存放注意事项 */}
        <View style={styles.noticeCard}>
          <Text style={styles.noticeTitle}>存放注意事项</Text>
          <Text style={styles.noticeText}>• 避免阳光直射和高温</Text>
          <Text style={styles.noticeText}>• 保持干燥通风环境</Text>
          <Text style={styles.noticeText}>• 儿童接触不到的地方</Text>
          <Text style={styles.noticeText}>• 定期检查药品有效期</Text>
          <Text style={styles.noticeText}>• 不同药品分类存放</Text>
        </View>
      </ScrollView>

      {/* 底部导航 */}
      <View style={styles.bottomNav}>
        <TouchableOpacity 
          style={[styles.navItem, styles.activeNavItem]} 
          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,
  },
  statsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  statItem: {
    alignItems: 'center',
  },
  statNumber: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#3b82f6',
  },
  statLabel: {
    fontSize: 12,
    color: '#64748b',
    marginTop: 4,
  },
  section: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    marginBottom: 16,
    overflow: 'hidden',
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  locationCard: {
    flexDirection: 'row',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  locationIcon: {
    width: 50,
    height: 50,
    borderRadius: 8,
    backgroundColor: '#e2e8f0',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  locationIconText: {
    fontSize: 24,
  },
  locationInfo: {
    flex: 1,
  },
  locationName: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 4,
  },
  locationDescription: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 4,
  },
  locationMedicineCount: {
    fontSize: 12,
    color: '#94a3b8',
  },
  locationAction: {
    justifyContent: 'center',
  },
  locationArrow: {
    fontSize: 16,
    color: '#94a3b8',
  },
  medicineCard: {
    flexDirection: 'row',
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#e2e8f0',
  },
  medicineIcon: {
    width: 50,
    height: 50,
    borderRadius: 8,
    backgroundColor: '#e2e8f0',
    alignItems: 'center',
    justifyContent: 'center',
    marginRight: 12,
  },
  medicineIconText: {
    fontSize: 24,
  },
  medicineInfo: {
    flex: 1,
  },
  medicineName: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 4,
  },
  medicineDosage: {
    fontSize: 14,
    color: '#64748b',
    marginBottom: 4,
  },
  medicineFrequency: {
    fontSize: 12,
    color: '#94a3b8',
    marginBottom: 4,
  },
  medicineMeta: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  medicineCategory: {
    fontSize: 12,
    color: '#10b981',
    backgroundColor: '#ecfdf5',
    paddingHorizontal: 6,
    paddingVertical: 2,
    borderRadius: 4,
  },
  medicineRemaining: {
    fontSize: 12,
    color: '#64748b',
  },
  medicineLocation: {
    justifyContent: 'center',
    alignItems: 'flex-end',
  },
  locationText: {
    fontSize: 12,
    color: '#64748b',
    textAlign: 'right',
  },
  locationMedicinesSection: {
    backgroundColor: '#f8fafc',
    padding: 12,
  },
  locationHeader: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 8,
  },
  locationHeaderIcon: {
    fontSize: 18,
    marginRight: 8,
  },
  locationHeaderText: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#1e293b',
  },
  medicineMiniCard: {
    backgroundColor: '#ffffff',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  medicineMiniName: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#1e293b',
  },
  medicineMiniDosage: {
    fontSize: 12,
    color: '#64748b',
  },
  medicineMiniRemaining: {
    fontSize: 12,
    color: '#94a3b8',
    marginTop: 4,
  },
  categoryStatsCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  categoryStatsTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  categoryStatsItems: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  categoryStatItem: {
    alignItems: 'center',
    flex: 1,
    padding: 8,
  },
  categoryStatIcon: {
    fontSize: 24,
    marginBottom: 4,
  },
  categoryStatText: {
    fontSize: 12,
    color: '#1e293b',
    marginBottom: 4,
  },
  categoryStatNumber: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#3b82f6',
  },
  managementCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  managementTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  managementItems: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  managementItem: {
    alignItems: 'center',
    flex: 1,
    padding: 8,
  },
  managementIcon: {
    fontSize: 24,
    marginBottom: 4,
  },
  managementText: {
    fontSize: 12,
    color: '#1e293b',
  },
  noticeCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 16,
    elevation: 1,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
  },
  noticeTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 12,
  },
  noticeText: {
    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,
  },
  activeNavItem: {
    paddingTop: 4,
    borderTopWidth: 2,
    borderTopColor: '#3b82f6',
  },
  navIcon: {
    fontSize: 20,
    color: '#94a3b8',
    marginBottom: 4,
  },
  activeNavIcon: {
    color: '#3b82f6',
  },
  navText: {
    fontSize: 12,
    color: '#94a3b8',
  },
  activeNavText: {
    color: '#3b82f6',
  },
});

export default MedicineLocationApp;

请添加图片描述



安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


打包

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

在这里插入图片描述

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

在这里插入图片描述

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

请添加图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐