ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

鸿蒙陪诊系统开发实战:基于ArkTS与Stage模型的全流程实现

鸿蒙陪诊系统开发实战:基于ArkTS与Stage模型的全流程实现 简介鸿蒙医院陪诊系统是基于HarmonyOS SDK 6.0.0开发的轻应用源码包面向希望学习HarmonyOS Stage模型和ArkTS语言的开发者解决医院陪诊场景中用户注册登录、病情选择与陪诊确认等流程的落地实现。包体共12个文件包含json与json5配置文件、ts源码文件、png图标及md说明文档等压缩包仅10KB结构清晰便于快速查阅。系统采用纯文字交互适配手机和平板并通过本地模拟数据实现无后端暂存适合作为鸿蒙应用开发的入门实践参考。目前已有138人学习资源附有编译运行指南和常见问题解决方案并给出持久化存储、网络对接等扩展方向可帮助读者理解页面路由、状态管理及项目模块划分思路提升实际开发能力。1. 从陪诊痛点出发为什么用鸿蒙做这套系统先聊点实际的。家里人前阵子去医院做检查我人在外地老人自己跑上跑下排队、取号、找科室、等报告折腾了一整天。当时我就在想如果有一款App能让家属远程下单、陪诊员接单、全程能看到服务进度是不是能省掉很多焦虑。市面上其实已经有商业陪诊平台了但作为开发者我更多考虑的是技术层面的事这类业务系统的核心链路是什么、数据模型怎么设计、在鸿蒙生态里能不能做成一个体验顺滑的原生应用。于是就有了这个项目——一套基于鸿蒙的医院陪诊系统覆盖患者端下单、陪诊员接单、订单状态流转、位置信息同步这些核心场景。这套项目适合三类人正在学鸿蒙开发ArkTS ArkUI Stage模型的开发者想找一个完整度高的实战项目练手准备做毕业设计、外包项目或面试作品的人陪诊系统业务边界清晰、功能模块完整很适合作为展示型项目对医疗健康类应用感兴趣的产品或全栈开发想看看鸿蒙端如何处理角色权限、订单状态机这类通用逻辑。技术选型上我用的是HarmonyOS NEXT的Stage模型语言是ArkTSUI层用ArkUI声明式写法数据层用系统自带的关系型数据库RDB和首选项Preferences。整套东西不依赖第三方SDK跑起来非常干净也方便你把核心逻辑抽出来换到自己项目里。我把项目代码完整梳理了一遍这篇文章会把架构设计、核心代码、踩过的坑一条条讲清楚。2. 陪诊业务到底在做什么模块拆解与工程结构很多人一上来就写页面结果是写了一堆UI却没有业务闭环。我做这类系统有个习惯先把角色、状态、数据流理清楚再动手写代码。2.1 三方角色与功能边界陪诊系统不是简单的“挂号App”它涉及到患者家属、陪诊员、平台管理三个视角。移动端主要承载前两者患者/家属端浏览医院、选择陪诊服务品类普通陪诊、老人陪诊、检查陪护、填写就诊信息、下单支付或预约、查看订单状态、联系陪诊员。陪诊员端查看可接订单、接单、开始服务、更新服务进度、完成订单。平台管理端我暂时放到了Web端做不在这个鸿蒙项目里展开。但从数据模型设计上必须给后续接入管理后台留好字段。比如订单表里的status状态字段要能覆盖从创建到完成的全生命周期。2.2 Stage模型下的工程目录规划项目创建时选的是Empty Ability模板包名我设为com.example.medicalescort。工程目录按模块拆分后长这样entry/src/main/ets/ ├── entryability/ │ └── EntryAbility.ets ├── pages/ │ ├── Index.ets │ ├── LoginPage.ets │ ├── HomePage.ets │ ├── OrderConfirmPage.ets │ ├── OrderListPage.ets │ └── EscortDetailPage.ets ├── model/ │ ├── OrderModel.ets │ ├── UserModel.ets │ └── HospitalModel.ets ├── common/ │ ├── constants/ │ │ └── CommonConstants.ets │ └── utils/ │ ├── RdbHelper.ets │ └── PreferencesUtil.etsmodel目录放数据类common/utils放数据库和偏好存储封装pages只放页面组件。这样做的原因是ArkTS对Observed和State的数据绑定要求比较严格数据模型与页面逻辑分离后状态管理会清晰很多尤其是订单状态更新后要同时刷新多个页面时你会感谢当初没有把所有数据写在页面里。2.3 UI框架与页面结构设定我选用了TabBar来承载两个角色的入口底部一个“患者端”Tab一个“陪诊员端”Tab。患者端首页是医院列表和搜索框陪诊员端首页是待接单列表。登录后通过PreferencesUtil保存当前角色标识userRoleTab页根据角色决定默认显示哪个页面。这里有个小设计点值得说两个Tab复用同一个Index.ets入口而不是各建一个页面。因为底部TabBar本身就是全局导航结构状态切来切去反而增加页面栈管理复杂度。登录后跳转到Index内部用Tabs组件完成两个容器的切换业务上互不干扰。3. 订单状态机与RDB数据表先让数据活起来订单是陪诊系统的核心实体。设计订单表时我参考了电商系统的状态流但做了简化陪诊场景没必要那么复杂。3.1 状态流转定义我定义了五个状态状态值含义说明0待接单患者下单成功等待陪诊员接单1已接单陪诊员已接单待开始服务2服务中陪诊员开始服务患者可看到进度3已完成服务结束订单归档4已取消患者或系统取消订单状态变更的约束关系在代码里写好不允许跳跃式流转。比如从“待接单”不能直接到“已完成”从“服务中”不能直接回到“待接单”。这些约束我在OrderModel里加了一个transitionMap数据对不上会直接报错避免脏数据写进数据库。3.2 建表语句与RDB封装鸿蒙的RDB接口用法和Android的SQLiteOpenHelper思路接近但API有些区别。核心是先获取RdbStore再执行建表语句。我在RdbHelper.ets里做了统一封装import { relationalStore } from kit.ArkData; import { CommonConstants } from ../constants/CommonConstants; const STORE_CONFIG: relationalStore.StoreConfig { name: medical_escort.db, securityLevel: relationalStore.SecurityLevel.S1 }; const CREATE_TABLE_ORDER CREATE TABLE IF NOT EXISTS patient_order ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT NOT NULL, patient_name TEXT NOT NULL, patient_phone TEXT, hospital_name TEXT NOT NULL, department TEXT, appointment_time TEXT NOT NULL, status INTEGER DEFAULT 0, escort_id INTEGER, create_time TEXT NOT NULL ) ; export class RdbHelper { private static instance: RdbHelper; private rdbStore: relationalStore.RdbStore | null null; static getInstance(): RdbHelper { if (!RdbHelper.instance) { RdbHelper.instance new RdbHelper(); } return RdbHelper.instance; } async init(context: Context): Promisevoid { if (this.rdbStore) return; this.rdbStore await relationalStore.getRdbStore(context, STORE_CONFIG); await this.rdbStore.executeSql(CREATE_TABLE_ORDER); } getStore(): relationalStore.RdbStore { if (!this.rdbStore) { throw new Error(RdbStore is not initialized); } return this.rdbStore; } }这里有个坑必须提醒getRdbStore是异步的如果你在页面aboutToAppear里直接调用很可能出现Store is not initialized的报错。我的做法是在EntryAbility.onWindowStageCreate阶段先初始化RdbHelper.getInstance().init(this.context)这样后续页面拿到的实例就是可用的。3.3 订单的唯一编号与写入逻辑订单号用时间戳加随机数的方式生成避免并发下重复。同时加了一个create_time字段排序时会用到。插入订单的代码封装在OrderModel.etsimport { relationalStore } from kit.ArkData; import { RdbHelper } from ../utils/RdbHelper; export class OrderModel { static async createOrder(order: OrderInfo): Promisenumber { const store RdbHelper.getInstance().getStore(); const orderNo Date.now().toString() Math.floor(Math.random() * 9000 1000).toString(); const values: relationalStore.ValuesBucket { order_no: orderNo, patient_name: order.patientName, patient_phone: order.patientPhone, hospital_name: order.hospitalName, department: order.department, appointment_time: order.appointmentTime, status: 0, create_time: new Date().toISOString() }; const rowId await store.insert(patient_order, values); return rowId; } }在真机上调试时如果你发现插入后查不到数据先看看securityLevel配置有没有少写。S1是最低安全等级适合普通业务数据如果设置成S3或S4部分设备会因为权限问题无法正常读写。4. 核心页面代码拆解从下单到接单的完整链路这章是整篇文章的重点我把一条完整业务链路串起来讲。4.1 首页医院列表与搜索首页用List组件渲染医院数据数据源我在项目里做了本地mock真实项目中可以替换成网络请求。核心代码如下Entry Component struct HomePage { State hospitalList: HospitalInfo[] [ { id: 1, name: 第一人民医院, level: 三甲, distance: 2.3km }, { id: 2, name: 中心医院, level: 三甲, distance: 3.8km }, { id: 3, name: 中医院, level: 三级, distance: 5.1km } ]; State keyword: string ; build() { Column() { TextInput({ placeholder: 搜索医院名称, text: this.keyword }) .onChange((value: string) { this.keyword value; }) .margin({ bottom: 12 }) List() { ForEach(this.hospitalList.filter(item item.name.includes(this.keyword) || this.keyword ), (hospital: HospitalInfo) { ListItem() { Column() { Row() { Text(hospital.name).fontSize(18).fontWeight(FontWeight.Bold) Blank() Text(hospital.level).fontSize(13).fontColor(#ffffff) .backgroundColor(#ff6b35).borderRadius(4).padding({ left: 6, right: 6 }) } .width(100%) Row() { Text(距离 hospital.distance).fontSize(13).fontColor(#888888) Blank() Text(选择陪诊).fontSize(14).fontColor(#ff6b35) .onClick(() { this.toConfirmPage(hospital); }) } .width(100%) .margin({ top: 8 }) } .padding(12) .backgroundColor(#ffffff) .borderRadius(8) .margin({ bottom: 10 }) } }, (hospital: HospitalInfo) hospital.id.toString()) } .width(100%) .layoutWeight(1) } .padding(16) .backgroundColor(#f5f5f5) } toConfirmPage(hospital: HospitalInfo) { // 路由跳转传递医院信息 } }这里有个细节ForEach的第三个参数是键值生成器必须保证每个item有唯一键。我直接用hospital.id.toString()如果你不加这个参数列表删除或插入时会出现渲染错位。4.2 预约下单页选好医院后进入确认页需要填写就诊人、手机号、科室和预约时间。页面用Form逻辑不太直观我直接用了多个TextInput和DatePicker的组合。Entry Component struct OrderConfirmPage { State hospitalName: string ; State patientName: string ; State patientPhone: string ; State department: string ; State appointmentTime: string 2025-06-01 09:00; build() { Column({ space: 14 }) { Text(this.hospitalName).fontSize(22).fontWeight(FontWeight.Bold) TextInput({ placeholder: 就诊人姓名, text: this.patientName }) .onChange((value: string) { this.patientName value; }) TextInput({ placeholder: 手机号, text: this.patientPhone }) .type(InputType.PhoneNumber) .onChange((value: string) { this.patientPhone value; }) TextInput({ placeholder: 就诊科室如心内科, text: this.department }) .onChange((value: string) { this.department value; }) Row() { Text(预约时间).fontSize(16) Blank() Text(this.appointmentTime).fontSize(16).fontColor(#333333) } .onClick(() { this.showDatePicker(); }) Button(提交预约) .width(100%) .onClick(() { this.submitOrder(); }) } .padding(16) .width(100%) } submitOrder() { if (!this.patientName || !this.patientPhone || !this.department) { // 弹Toast提示 return; } const order: OrderInfo { patientName: this.patientName, patientPhone: this.patientPhone, hospitalName: this.hospitalName, department: this.department, appointmentTime: this.appointmentTime }; OrderModel.createOrder(order).then((rowId: number) { if (rowId 0) { // 跳转到订单列表页 } }); } }表单校验那里我偷了个懒只判断了非空。真实项目中建议用正则做手机号校验再加一个State的submitDisabled判断当必填项不全时按钮置灰体验会更好。这样也不会让用户点完按钮才弹错误提示。4.3 陪诊员端待接单与接单逻辑陪诊员端的关键是“抢单”体验不能刷半天没反应。我用RDB查询所有status0的订单按create_time倒序排列配合下拉刷新。Entry Component struct EscortHomePage { State pendingOrders: OrderInfo[] []; aboutToAppear() { this.loadPendingOrders(); } async loadPendingOrders() { const store RdbHelper.getInstance().getStore(); const predicates new relationalStore.RdbPredicates(patient_order); predicates.equalTo(status, 0) .orderByDesc(create_time); const resultSet await store.query(predicates); const orders: OrderInfo[] []; while (resultSet.goToNextRow()) { orders.push(this.parseOrder(resultSet)); } resultSet.close(); this.pendingOrders orders; } acceptOrder(orderId: number) { const store RdbHelper.getInstance().getStore(); const predicates new relationalStore.RdbPredicates(patient_order); predicates.equalTo(id, orderId).and().equalTo(status, 0); const values: relationalStore.ValuesBucket { status: 1 }; store.update(values, predicates).then((rows: number) { if (rows 0) { // 更新成功刷新列表 this.loadPendingOrders(); } else { // 订单已被别人接走 } }); } }注意acceptOrder里我加了and().equalTo(status, 0)条件。这就是并发控制如果两个陪诊员同时点接单后执行update的人匹配不到status0的记录rows会返回0自然就不会出现一单被两个人接走的情况。这个点在面试里也可以作为亮点讲出来体现你有并发意识。4.4 服务进行中状态更新与导航跳转接单后订单进入“已接单”陪诊员可以点击“开始服务”将状态改为2。服务过程中如果需要导航去医院可以用系统提供的Intent调起地图应用核心代码就几行import { wantConstant } from kit.AbilityKit; function openMapNavigation(address: string) { const want { action: wantConstant.Action.VIEW, uri: maps://app?destination${encodeURIComponent(address)} }; (getContext(this) as common.UIAbilityContext).startAbility(want); }说实话鸿蒙地图生态目前没有Android那么丰富很多第三方地图SDK还在适配阶段。所以我选择了跳转系统地图应用的方案不用自己集成SDK接入成本最低。如果你要在应用内做实时定位追踪那就得用ohos.geoLocationManager了这个可以在项目第二阶段扩展。5. 真机调试与模拟器这几个坑我花了一晚上才爬出来这部分是纯经验分享了。项目跑通Page级别的调试验证不难难的是把流程完整走一遍时遇到的各种环境问题。5.1 模拟器的平台限制问题最典型的一个运行设备不兼容鸿蒙模拟器目前只能在arm64平台运行jsvm。这句话我调试时看到过不止一次。Intel芯片的电脑想靠模拟器跑项目多半会遇到这个提示。如果你是Windows Intel芯片别死磕模拟器直接申请华为真机或者用远程真机服务效率高得多。项目里RDB数据库相关操作放到模拟器和真机上的表现也不完全一致最终验证还是以真机为准。5.2 HAR模块封装与so库的问题项目做多模块拆分时你大概率会用到HARHarmony Archive来复用代码比如把RdbHelper、PreferencesUtil、网络请求封装成一个公共模块。这里我踩过坑如果你在HAR里封装了带so库的SDK比如某些地图SDK、加密库在主工程引用时会经常出现加载失败的问题。排查方向是确认so库的CPU架构是否匹配以及HAR的oh-package.json5里的依赖是否声明完整。在鸿蒙开发里“har封装so”是一个非常经典的坑点搜索词碰到问题先检查依赖链。5.3 断点调试不生效的问题很多刚转鸿蒙开发的人会问“鸿蒙怎么打断点”。其实DevEco Studio的断点调试和Android Studio很接近点行号断点然后点击Debug运行即可。但如果你发现断点根本不生效大概率是以下原因之一编译模式是Release要切到Debug模式运行断点打在了被混淆或者编译优化掉的代码行比如空方法体运行的是模拟器某些场景下断点会延迟或失效我个人的习惯是UI渲染类问题用日志输出定位业务逻辑类问题再打断点。日志其实是最快的调试方式尤其在ArkUI里console.info输出清晰还能过滤关键字。打断点调试在跨Ability、跨进程场景下有时会卡住日志定位更省心。5.4 ArkTS的语法限制最后提醒一个很多新手的尴尬点ArkTS不是完全自由的JavaScript/TypeScript它支持严格模式any类型基本不能用对象字面量需要显式声明接口类型。我在写ResultSet解析时最开始用了resultSet.getLong(columnIndex)的返回值直接赋值给number编译就报错了。正确做法是先定义一个接口比如OrderInfo解析时按字段类型强制转换interface OrderInfo { id: number; orderNo: string; patientName: string; hospitalName: string; status: number; }这种限制一开始会觉得束手束脚但写多了你会发现它其实能帮你规避很多隐藏的类型错误尤其是团队协作的项目里接口定义清楚了其他人接手时认知成本低得多。6. 这个项目还能怎么扩展写完一版陪诊系统之后我自己复盘了一下结合热搜词里大家关注的鸿蒙开发方向觉得这几个扩展点非常值得继续做接入鸿蒙的位置服务能力让陪诊员位置实时同步给患者端家属在手机上能看到陪诊员到了哪个位置这个对陪诊服务的信任感提升特别大引入卡片服务Form ExtensionAbility把订单状态做成桌面卡片患者不用开App就能看到“陪诊员已接单”“正在前往医院”这些关键状态这个在鸿蒙上是原生优势Android实现起来反而麻烦把订单数据接入云端用云函数做订单分配算法实现平台端的自动派单逻辑。目前本地RDB存数据适合demo但真要商用必须走云端。陪诊系统是一个很典型的O2O业务模型它包含的订单状态机、角色权限、数据持久化、异步刷新这些知识点放到外卖、家政、代驾这些项目里完全能复用。你在鸿蒙上把这个项目吃透换到别的业务场景差别其实只是字段和页面不同底层架构逻辑是一致的。我个人的建议是不要满足于把页面UI画出来而是要从订单数据的完整流转角度去梳理你的代码。能想清楚“什么时候数据从哪来、到哪去、状态怎么变”这套系统的设计能力才算真正内化了。动手改代码比看十篇文章都管用把这个陪诊系统放进你的项目清单里跑起来再从扩展点上逐个迭代比空谈鸿蒙理论强得多。本文还有配套的精品资源点击获取
返回列表