首页 / 资讯中心 / 文章详情

鸿蒙Stage模型+ArkTS实战:健身预约系统全链路开发解析

鸿蒙Stage模型+ArkTS实战:健身预约系统全链路开发解析 ★ FEATURED ARTICLE
简介这套鸿蒙全民健身预约系统应用开发源码面向鸿蒙移动应用开发者与高校实训学员提供一套包含后台管理端与移动端的完整健身预约项目。应用覆盖用户管理、场馆动态、运动常识、预订管理、预约通知等核心模块支持查看可预约的场馆、教练与陪练并实现多端流转与协同交互界面简洁兼顾隐私与数据安全。压缩包共三百五十五个文件大小约十兆其中包含九十九张图片素材、六十三个代码文件、三十八个配置文件等结构清晰便于按模块研读或二次开发。已有五百二十七人学习下载。作为首次发布的移动端实训作品该源码还原了真实预约业务链路适合作为课程设计、毕业设计或鸿蒙应用开发入门进阶的参考项目可帮助读者快速理解鸿蒙工程组织方式与预约类应用的设计思路。1. 全民健身预约系统到底是什么一个能用鸿蒙原生能力直接跑起来的预约闭环一个运动场馆的预约场景远比想象中琐碎。打开健身App用户要的不是看广告而是快速知道今天哪个时段有空场、约上之后到场不用报手机号、临时有事能取消。这套鸿蒙全民健身预约系统App做的就是这件事从场馆列表、课程时段选择、预约提交到订单状态管理全部在鸿蒙设备上原生跑通不依赖浏览器壳也不借助第三方小程序容器。适合两种人拿它当起点。一是刚接触HarmonyOS应用开发、想找个完整项目练手的开发者——它把Stage模型、ArkTS状态管理、路由跳转和本地持久化串成一条完整链路比自己零散拼Demo更接近真实App的结构。二是确实要做健身场馆、体育场地预约类产品的人这套代码相当于一个可运行的前端骨架业务字段、页面流程都能直接改造成自己的预约系统。全文围绕一个核心展开一次预约请求在鸿蒙App里从首页点击到订单生成数据是怎么流转的。下文会按工程目录、界面层、状态层、数据层逐层拆再给你真机运行的具体步骤和踩坑记录。2. 技术架构选型为什么用Stage模型加ArkTS做预约闭环先看整个项目的技术骨架。HarmonyOS从API 9开始全面推行Stage模型这套预约系统就是按Stage模型的标准姿势组织的。Stage模型和老的FA模型最大的区别在于代码必须模块化每个Ability都有独立的生命周期UIAbility负责界面DataAbility或者模块化的数据层负责业务数据组件不能越权直接访问系统资源。2.1 工程形态Ability、Page与组件三层分工打开工程先看AppScope目录下的app.json5里面声明了应用包名和图标这个文件相当于整个App的门面配置。真正的业务代码在entry模块下entry/src/main/ets里分成几个核心目录entryability装着唯一的UIAbility入口pages目录放页面级组件common放公共工具和常量。三个层次的职责边界很清晰。UIAbility只做两件事onCreate里初始化全局数据onWindowStageCreate里加载首页。页面组件负责拼装UI和接收用户事件公共模块负责被多个页面复用的方法比如日期格式化、预约状态映射这类纯函数。2.2 状态管理选型State、Prop与Link的配合方式预约流程里最麻烦的不是页面多而是状态分散。选课时段要同步到确认页确认页改了备注要回传首页。如果全部用全局变量页面一销毁数据就没了全部用持久化每改一个字段都写磁盘又太重。这套工程的做法是分了两层页面内部用State管局部状态页面间用router带参数传递订单落库用Preferences持久化。最顶层不需要AppStorage级别的全局状态因为预约流程的共享数据集中在一次操作会话里路由参数足够承担。Entry Component struct BookingPage { State selectTime: string State courtType: number 1 // 1-羽毛球 2-篮球 3-健身房 State remainingCount: number 0 build() { Column() { Text(this.selectTime ? 已选: ${this.selectTime} : 请选择时段) ForEach(this.timeSlots, (slot: string) { Button(slot).onClick(() { this.selectTime slot this.remainingCount 20 - Math.floor(Math.random() * 5) }) }) } } }这段代码演示了页面内部状态的管理方式。State装饰的变量一旦变化build方法自动重新执行UI跟着刷新不需要手动操作DOM。timeSlots是页面里定义好的静态时段数组点击时段按钮后既更新选中文案也模拟计算出剩余名额。2.3 数据持久化Preferences而不是关系型数据库的原因预约记录、用户收藏的场馆信息这类数据特点是单条数据小、读写频率不高、结构固定。鸿蒙自带的关系型数据库RDB功能强但初始化代码长分布式数据库在这个单设备场景里没必要。项目选用ohos.data.preferences里的Preferences它本质是个轻量级KV存储适合存JSON字符串。import preferences from ohos.data.preferences import { BusinessError } from ohos.base const PREF_NAME fitness_pref export async function saveBookingRecord(context: Context, key: string, value: object) { const pref await preferences.getPreferences(context, PREF_NAME) await pref.put(key, JSON.stringify(value)) await pref.flush() }getPreferences拿到的是单例存储实例put写入后必须调用flush才真正落盘。这里把存储逻辑封装成独立函数页面层只管传context进来不需要关心Preferences的实例管理。强调一下flush这一步开发中容易漏漏了之后数据在内存里存在重启App就不见了。3. 核心模块拆解一次预约从点击到订单生成的全链路预约是整套系统的用例主线从首页场馆卡片开始到我的预约页里看到订单为止中间跨了四个页面和两层数据写操作。从代码组织的角度可以拆成入口分发、路由传参、数据回写三个环节去看。3.1 首页场馆列表数据渲染与点击分发首页没有采用复杂的懒加载框架直接用List组件加ForEach渲染场馆列表。每个场馆卡片是独立子组件父组件通过Prop把场馆数据传下去点击事件在最上层统一处理。这样做的原因是预约场馆的数据量级在几十条以内不需要LazyForEach按需创建简单方案反而更好维护。import router from ohos.router Entry Component struct HomePage { private venues: Arrayobject [ { id: 1, name: 羽毛球馆A, type: 羽毛球, price: 35 }, { id: 2, name: 篮球馆B, type: 篮球, price: 50 } ] build() { List() { ForEach(this.venues, (item: any) { ListItem() { Column() { Text(item.name).fontSize(18).fontWeight(FontWeight.Medium) Text(${item.type} | ¥${item.price}/小时) } .onClick(() { router.pushUrl({ url: pages/BookingPage, params: { venueId: item.id, venueName: item.name } }) }) } }, (item: any) item.id.toString()) } } }router.pushUrl里的params对象是把场馆ID和名称带给预约页面的关键。注意ForEach的第三个参数是键生成器这里用id做键保证列表项复用正确避免因为场馆名称重复导致界面状态错乱。这是一个新手经常翻车的地方直接不给键生成器开发工具会警告甚至出现渲染异常。3.2 预约页时段选择、名额余量显示与校验预约页是整条业务链路里逻辑最重的地方。几个核心逻辑串在一起日期默认今天且不能选过去时段以半小时为粒度展示选中时段后展示对应剩余名额名额不足时禁止提交。这些规则不复杂但要拆成独立方法去维护不然build方法会膨胀到没法看。Entry Component struct BookingPage { State currentDate: string State timeOption: string State quota: number 0 State quotaEnough: boolean true private allSlots: string[] [08:00-08:30, 08:30-09:00, 09:00-09:30] aboutToAppear() { const now new Date() this.currentDate ${now.getMonth() 1}月${now.getDate()}日 } checkQuota(slot: string): number { // 模拟从本地数据源读取该时段余量真实项目替换为远端接口 return this.allSlots.indexOf(slot) 0 ? 6 : 0 } build() { Column({ space: 12 }) { Text(预约日期: ${this.currentDate}) ForEach(this.allSlots, (slot: string) { Button(slot) .enabled(this.checkQuota(slot) 0) .onClick(() { this.timeOption slot this.quota this.checkQuota(slot) this.quotaEnough this.quota 0 }) }) Button(提交预约) .enabled(this.timeOption ! this.quotaEnough) } } }这个页面里enabled属性的使用值得注意。Button的enabled既用来控制时段按钮余量为0的时段直接置灰也控制提交按钮没选时段不能提交。预约系统的交互底线就在这里不允许用户走到提交订单那一步才发现时段满了必须在选择阶段就拦截掉。3.3 订单确认页参数接收、补充信息和本地落库预约确认页负责把前面选的时段信息变成一条真正的订单记录。路由参数在这个页面通过router.getParams()取出来补充用户手机号之后组装成订单对象写入Preferences。订单写本地而不是远端接口是因为这套源码默认无服务端运行所有数据落设备本地。Entry Component struct ConfirmPage { State phone: string State orderNo: string private venueId: number 0 aboutToAppear() { const params router.getParams() as Recordstring, Object if (params) { this.venueId params.venueId as number } this.orderNo FT${Date.now()}${Math.floor(Math.random() * 1000)} } submitOrder() { const order { orderNo: this.orderNo, venueId: this.venueId, phone: this.phone, status: 已确认 } saveBookingRecord(getContext(this), order_${this.orderNo}, order) router.pushUrl({ url: pages/MyBookingPage }) } build() { Column({ space: 16 }) { Text(订单号: ${this.orderNo}) TextInput({ placeholder: 请输入手机号 }) .onChange((value: string) { this.phone value }) Button(确认提交) .enabled(this.phone.length 11) .onClick(() this.submitOrder()) } } }订单号用时间戳加随机数生成的做法先天缺点是不能保证绝对唯一但在这个量级的个人项目里够用。如果后面要接服务端换成服务端生成的订单号会更稳妥。TextInput的onChange回调不能漏预约场景里手机号是提交前的必填项按钮的enabled直接绑定手机号长度算是最简单可用的前校验方案。3.4 我的预约页订单列表读取与状态展示最后一个页面是订单落库后的闭环展示。从Preferences里把所有order_开头的key读出来解析JSON后展示成列表。这个页面的代码思路是读取的时候不确认有多少条订单统一用getAllSync取全量再按key过滤。Entry Component struct MyBookingPage { State orders: object[] [] aboutToAppear() { this.loadOrders() } loadOrders() { const pref preferences.getPreferencesSync(getContext(this), fitness_pref) const allValues pref.getAllSync() if (!allValues) return const list: object[] [] for (const key of Object.keys(allValues)) { if (key.startsWith(order_)) { list.push(JSON.parse(allValues[key] as string)) } } this.orders list } build() { List() { ForEach(this.orders, (order: any, index) { ListItem() { Row() { Text(order.venueName).fontSize(16) Text(order.status).fontSize(14).fontColor(order.status 已确认 ? #0A59F7 : #999999) } } }, (order: any, index) index.toString()) } } }需要注意loadOrders里用的是getAllSync同步接口在page的aboutToAppear生命周期里调用不会阻塞太久因为订单数据量很小。数据量涨到几百条之后建议换成异步getAll加Loading状态这是从Demo走向产品化必然要改的一步。键过滤用startsWith来确定订单数据坏处是其他业务字段只要前缀带order_也会被拉进来后面做二次开发时最好把订单单独存一个keyvalue里再放数组避免遍历全表。4. 从源码到真机运行环境搭建、跑通流程与避坑记录拿到源码第一件事不是读代码而是先让它在真机或模拟器上跑起来。鸿蒙项目跟Android项目不一样不能用Android Studio直接打开必须用DevEco Studio。版本对应关系是固定的DevEco Studio的版本决定了能编译哪个API版本的工程。4.1 环境准备与工程导入的具体操作先装DevEco Studio装的时候勾选HarmonyOS SDK组件。打开SDK Manager把API版本切到和工程兼容的版本。当前这套工程在API 9的Stage模型下开发用较新版本的DevEco Studio打开后首次构建会提示SDK版本不匹配这时候进File Project Structure里确认SDK版本IDE会给出自动修正选项。导入工程用File Open直接选到entry所在的根目录不要只选entry子目录否则工程配置会丢失。等IDE完成Sync之后检查两个位置entry/build-profile.json里的compileSdkVersion是否本地已安装signingConfigs是否已配置。实操中最常见的失败点就是签名配置缺失——鸿蒙真机调试必须要有签名否则安装阶段直接报错。4.2 真机调试前的关键设置真机调试开开发者模式这个不多说。鸿蒙特有的一步是在DevEco Studio的File Project Structure Signing Configs里勾选Automatically generate signatureIDE拿着你的华为账号做自动签名。这里需要本机登录华为账号如果账号没实名认证自动签名会失败这是环境准备里最容易卡住的地方。hdc list targets命令行先确认设备连接状态。hdc是鸿蒙的命令行工具作用和adb在Android生态里一样。看到设备列表后直接点DevEco Studio的Run按钮IDE会完成编译、签名、安装、启动四个步骤。如果Run按钮点完一直卡在Launching多半是签名没生成成功去检查签名配置页的报错提示。4.3 避坑记录跑鸿蒙预约项目时最容易翻车的五个地方先说最常见的签名同步问题。现象代码完全没问题编译也通过但装到手机上提示签名错误。原因工程里的自动签名文件是生成时的和当前设备不匹配。解决打开Signing Configs重新勾选自动签名让IDE重新生成不要手动拷贝别人的签名文件那样会浪费一小时起步。再说Preferences写入后数据丢失。现象提交订单成功杀掉App重进我的预约页数据消失了。原因调了put但没调flush。解决每次put后强制flush或者在封装的储蓄函数里把flush写在put后面这个习惯值得一直保持。第三个是路由跳转参数类型翻车。现象从首页跳到预约页venueId取出来显示成了undefined。原因router.getParams()返回的对象里的值被IDE识别成了unknown类型直接当成number用就崩了。解决用as Recordstring, Object做类型断言后再取值配合console.log打印确认类型不要想当然。第四个是ForEach缺key生成器。现象场馆列表滑动后按钮点击没有反应或者界面出现闪烁。原因列表项没有稳定的键组件复用时状态错乱。解决给ForEach第三个参数传稳定键场馆数据用id没有id的数据用下标但下标方案在列表排序或删除后会产生更隐蔽的问题。第五个是时区显示偏差。现象页面显示的日期和手机系统日期对不上预约时间整体偏移。原因new Date()拿的是设备本地时间但模拟器时区常常是格林尼治。解决真机调试为主模拟器只验证布局和基础交互。这个坑在预约类项目里尤其要重视用户迟到了会投诉的。5. 二次开发把单机预约Demo改造成生产级预约闭环这套源码的定位是能跑的骨架要真正上线还有很多硬骨头要啃。从工程现状往前推最优先改的不是UI而是数据链路——它现在是纯本地存储离真实业务还差一个服务端。5.1 第一步改造所有本地数据操作替换为网络请求Preferences在当前架构里承担了订单存储的职责二次开发时先抽象一个数据服务层。这里给一个基本的接口思路export class BookingApi { static getVenueList(): PromiseVenue[] { return request(GET, /api/venues) } static createOrder(order: OrderPayload): PromiseOrder { return request(POST, /api/orders, order) } }把页面里的saveBookingRecord调用全部替换成BookingApi.createOrder页面代码的改动量可以控制在很小的范围内因为页面层只依赖saveBookingRecord这个函数名不关心它内部是写本地还是发网络请求。5.2 第二步改造预约名额实时校验单机版用随机数模拟余量这是Demo最大的软肋。改成真实业务后提交预约瞬间必须向后端确认名额并锁定。前端要做的是在提交按钮点击后进入loading态拿到后端成功响应才算下单成功失败则回滚按钮状态并提示用户重选时段。5.3 第三步改造补充消息通知和取消流程预约类App的完整闭环至少还缺两块预约成功后的提醒通知以及爽约或计划变化的取消通道。鸿蒙侧建议用系统通知能力做预约成功提醒用长按订单弹出取消确认框的方式处理取消取消后刷新列表状态。这套逻辑接在现有订单页的状态字段上扩展成本不高。5.4 第四步改造状态机代替散落的字符串判断当前代码里用status字段存中文描述这在Demo里没问题一旦接入后端就会遇到显示文案和业务状态耦合的问题。建议改成数字枚举前端维护一张映射表后端只传状态码。从那以后我每次做预约类项目都会强制先定一张状态机表再动手写页面把已确认、已完成、已取消、已过期几种状态和用户可执行操作的关系理清楚页面侧漏掉任何一个分支都会在真实运营时冒出来预约功能最怕的就是状态不一致。这套工程的价值在于把鸿蒙应用开发从入门到上手的完整流程压缩了一遍下载后花一晚上把代码从头跟到尾再按上面的思路改一个功能点比单独看文档领会深得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站