React Native鸿蒙 基于HarmonyOS API 24 如何实现“位置-药品”双层关联数据模型设计、嵌套列表渲染逻辑
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 管理三个核心数据源:medicines、locations 和 medicationRecords。这种函数式状态管理方式在 React Native 和 HarmonyOS ArkUI 中都得到了很好的支持,是跨端开发的推荐方案。
在 HarmonyOS 跨端适配中,useState Hook 可以直接映射到 ArkUI 的 @State 装饰器,实现状态的响应式更新。值得注意的是,当状态数据量较大时,建议使用 useMemo 或 useCallback 进行性能优化,避免不必要的组件重渲染。应用中的 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 组件显示药品详情。这种简洁的交互设计符合移动端应用的设计规范,在跨端开发中需要注意:
Alert.alert在 HarmonyOS 中对应dialog.showAlertDialogAPI,需要进行适配- 触摸反馈效果在不同平台上的表现可能存在差异,建议使用统一的反馈机制
- 药品卡片的布局设计考虑了响应式需求,通过
StyleSheet中的width计算实现了适配不同屏幕尺寸的效果
应用中的 medicineCard 组件采用了左右布局,左侧显示药品图标,右侧显示详细信息,这种布局方式在 React Native 和 HarmonyOS 上都能获得良好的显示效果。药品状态通过 remaining 属性直观展示,便于用户了解药品库存情况,这种设计思路值得在跨端应用中推广。
列表渲染
应用在列表渲染中使用了 FlatList 组件,这是 React Native 处理长列表的最佳实践。FlatList 实现了虚拟列表技术,只渲染可见区域内的组件,大幅提升了长列表的性能。在 HarmonyOS 跨端开发中,FlatList 同样能获得良好的性能表现,但需要注意:
- 确保为每个列表项提供唯一的
key属性,避免不必要的组件重渲染 - 复杂列表项建议使用
memo或useMemo进行优化 - 图片资源(如示例中的 base64 图标)应合理管理,在生产环境中建议使用图片文件而非 base64 编码,以减少内存占用
应用中的 getMedicinesForLocation 函数通过过滤算法实现了数据关联查询,在数据量较大时可能成为性能瓶颈。建议在跨端应用中考虑使用更高效的数据结构(如 Map)或引入状态管理库(如 Redux、MobX)来优化数据查询和状态更新。
-
组件拆分与复用:当前应用将所有逻辑集中在一个组件中,建议将药品卡片、位置列表等功能拆分为独立的子组件,提高代码复用性和可维护性。在跨端开发中,组件化设计尤为重要,便于在不同平台上进行差异化适配。
-
API 封装与平台适配:应用中直接使用了
Alert和Dimensions等 React Native API,建议将这些 API 封装为统一的工具函数,处理不同平台的差异。例如:
// 跨端弹窗工具函数
export const showAlert = (title: string, message: string) => {
// 在不同平台上调用不同的 API
if (Platform.OS === 'harmony') {
// HarmonyOS 实现
} else {
// React Native 实现
Alert.alert(title, message);
}
};
-
样式主题化:当前应用的样式直接定义在
StyleSheet中,建议提取样式变量为主题常量,便于统一管理和主题切换。在跨端应用中,主题化设计可以提高应用的可定制性,适应不同平台的设计规范。 -
数据持久化:应用中的数据当前存储在内存中,建议引入本地存储方案(如 AsyncStorage、SQLite)实现数据持久化。在 HarmonyOS 跨端开发中,可以使用
@ohos.data.storage或@ohos.data.relationalStore实现类似功能。 -
错误处理与边界情况:应用中缺少完善的错误处理机制,建议添加 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号),既保证信息可见性,又不抢夺位置名称的视觉焦点,符合医疗应用“核心信息优先”的设计原则。
FlatList 的 showsVerticalScrollIndicator={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 |
data → listData,renderItem → itemGenerator,适配位置列表的长列表场景 |
嵌套 map 渲染(locations.map + locationMedicines.map) |
嵌套 ForEach |
外层 ForEach 遍历位置,内层 ForEach 遍历药品,保持嵌套视图结构一致 |
TouchableOpacity 位置卡片交互 |
Button 组件 |
onPress → onClick,设置 backgroundColor: Transparent 去除默认样式,保证位置卡片交互体验一致 |
| 条件渲染(locationMedicines.length === 0) | 条件渲染(if 语句) | 完全复用空位置过滤逻辑,避免鸿蒙端空视图展示 |
| 嵌套视图样式(locationMedicinesSection) | 嵌套容器样式 | View → Column/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工程目录去:

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

更多推荐



所有评论(0)