HarmonyOS ArkTS 实战:实现一个校园疫苗接种与健康管理应用
HarmonyOS ArkTS 实战:从零实现校园疫苗接种与健康管理应用
适用版本:HarmonyOS API 24 + DevEco Studio 5.0
目录
- 一、项目背景与效果预览
- 二、技术栈与开发环境
- 三、需求分析与功能架构
- 四、数据结构与服务层设计
- 五、核心功能实现(完整代码)
- 六、UI 界面设计与实现(完整组件)
- 七、完整主页面代码(Index.ets)
- 八、运行与调试
- 九、项目总结与扩展思路
一、项目背景与效果预览
1.1 痛点场景
“早上8点,辅导员在群里@全体成员:请于10点前完成健康打卡,未打卡者通报批评!”——这种场景在校园中屡见不鲜。疫苗接种第二针何时打?感冒了该不该去校医院?体温异常如何快速上报?校园健康管理亟需一款集成化、智能化的移动应用。
本项目基于 HarmonyOS ArkTS 开发,覆盖 疫苗预约、每日打卡、体温监测、健康码、就医记录 等全流程,让健康管理数字化、可视化。
1.2 运行效果(模拟器截图描述)
- 主界面:顶部为动态健康码卡片(绿/黄/红三态,颜色渐变动画);中间为打卡大按钮(未打卡时带有脉动呼吸效果);下方为疫苗接种时间线(清晰显示已接种、已预约、未接种状态);再下方为近7天体温趋势折线图;底部为快捷入口(校医院预约、核酸查询、就医记录、用药提醒)。
- 交互反馈:打卡时弹出底部动作面板(含体温输入、症状多选、位置选择),提交后 Toast 提示并更新健康码,同时连续打卡天数+1;预约疫苗时弹出日期选择对话框;体温异常时顶部横幅闪烁提醒并震动。
二、技术栈与开发环境
| 技术项 | 说明 |
|---|---|
| 开发语言 | ArkTS(TypeScript 超集) |
| UI 框架 | ArkUI 声明式开发范式 |
| 状态管理 | @State / @Provide / @Consume / @Observed |
| 布局方式 | Flex + List + 自定义时间线 + Chart |
| 数据持久化 | @ohos.data.preferences |
| 路由管理 | @ohos.router |
| 图表组件 | @ohos.arkui.advanced(Chart) |
| 弹窗组件 | @ohos.prompt / @ohos.dialog |
| 开发工具 | DevEco Studio 5.0+ |
| SDK 版本 | API 24 及以上 |
三、需求分析与功能架构
3.1 核心功能清单(细化至用户故事)
- 健康码展示:根据当日体温和打卡状态自动生成绿/黄/红码,动态更新。
- 每日健康打卡:用户填写体温、症状(多选)、所在位置,提交后记录打卡历史,并累计连续打卡天数。
- 疫苗接种管理:查看所有疫苗的接种状态(已接种/已预约/未接种),预约未接种疫苗(选择日期),取消已预约。
- 体温趋势分析:以折线图展示近7天体温变化,同时统计正常率和平均体温。
- 异常预警:体温≥37.3℃时,顶部弹出黄色横幅并调用系统震动,提醒用户就医。
- 数据持久化:所有打卡记录、疫苗状态保存到 Preferences,退出应用后数据不丢失。
- 快捷功能路由:点击“校医院预约”等入口,跳转到独立页面(模拟),展示完整应用架构。
3.2 业务流程图(以打卡为例)
用户点击“今日健康打卡”
→ 弹出底部动作面板
→ 输入体温(默认36.5)
→ 选择症状(多选)
→ 选择位置(学校/校外)
→ 点击“提交”
→ 校验(是否重复打卡、体温范围)
→ 生成打卡记录,更新连续天数
→ 若体温≥37.3,健康码变黄,触发预警
→ 保存到 Preferences
→ 提示成功,关闭面板。
四、数据结构与服务层设计
4.1 核心数据模型(完整定义)
// model/Vaccine.ets
export interface Vaccine {
id: number;
name: string; // 疫苗名称
factory: string; // 生产厂家
dose: number; // 第几剂
inoculateTime: string; // 接种时间(已接种时有效)
location: string; // 接种地点
doctor: string; // 接种医生
status: '已接种' | '已预约' | '未接种';
nextDueTime: string; // 下一针建议时间/截止日期
reminderDate?: string; // 到期提醒日期(新增)
}
// model/HealthRecord.ets
export interface HealthRecord {
id: number;
date: string; // 日期 yyyy-MM-dd
temperature: number;
healthStatus: '健康' | '异常' | '疑似';
location: '学校' | '校外' | '其他';
isReported: boolean;
symptoms: string[]; // 症状列表
}
// model/UserInfo.ets
export interface UserInfo {
name: string;
studentId: string;
college: string;
avatarColor: string; // 头像背景色
}
4.2 服务层(Service)设计
我们将数据操作抽离为服务类,便于日后对接真实网络 API。
// service/BaseService.ets
import preferences from '@ohos.data.preferences';
import { BusinessError } from '@ohos.base';
export abstract class BaseService<T> {
protected prefName: string;
protected key: string;
constructor(prefName: string, key: string) {
this.prefName = prefName;
this.key = key;
}
protected async getPreferences(): Promise<preferences.Preferences> {
return await preferences.getPreferences(this.prefName);
}
protected async loadData(): Promise<T[]> {
const pref = await this.getPreferences();
const json = await pref.get(this.key, '[]') as string;
return JSON.parse(json) as T[];
}
protected async saveData(data: T[]): Promise<void> {
const pref = await this.getPreferences();
await pref.put(this.key, JSON.stringify(data));
await pref.flush();
}
abstract fetch(): Promise<T[]>;
abstract add(item: T): Promise<T[]>;
abstract update(id: number, newItem: T): Promise<T[]>;
abstract delete(id: number): Promise<T[]>;
}
// service/VaccineService.ets
import { BaseService } from './BaseService';
import { Vaccine } from '../model/Vaccine';
class VaccineService extends BaseService<Vaccine> {
constructor() {
super('HealthPrefs', 'vaccines');
}
async fetch(): Promise<Vaccine[]> {
// 模拟网络延迟,实际可改为网络请求
await this.simulateDelay(300);
const data = await this.loadData();
if (data.length === 0) {
// 无数据时初始化模拟数据
const mock = this.getMockData();
await this.saveData(mock);
return mock;
}
return data;
}
async add(item: Vaccine): Promise<Vaccine[]> {
const list = await this.loadData();
list.push(item);
await this.saveData(list);
return list;
}
async update(id: number, newItem: Vaccine): Promise<Vaccine[]> {
const list = await this.loadData();
const index = list.findIndex(v => v.id === id);
if (index !== -1) {
list[index] = newItem;
await this.saveData(list);
}
return list;
}
async delete(id: number): Promise<Vaccine[]> {
const list = await this.loadData();
const filtered = list.filter(v => v.id !== id);
await this.saveData(filtered);
return filtered;
}
private getMockData(): Vaccine[] {
return [
{ id: 1, name: '新冠疫苗', factory: '北京生物', dose: 3, inoculateTime: '2025-12-15', location: '校医院', doctor: '王医生', status: '已接种', nextDueTime: '' },
{ id: 2, name: 'HPV疫苗', factory: '默沙东', dose: 1, inoculateTime: '', location: '校医院', doctor: '', status: '未接种', nextDueTime: '建议尽快接种' },
{ id: 3, name: '流感疫苗', factory: '华兰生物', dose: 1, inoculateTime: '', location: '校医院', doctor: '', status: '未接种', nextDueTime: '2026-10-01前' },
{ id: 4, name: '乙肝疫苗', factory: '深圳康泰', dose: 3, inoculateTime: '2023-09-10', location: '校医院', doctor: '李医生', status: '已接种', nextDueTime: '' },
];
}
private simulateDelay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export const vaccineService = new VaccineService();
同理实现 HealthRecordService(用于打卡记录),UserService(用户信息)。篇幅原因,不一一列出,但完整项目中会包含。
五、核心功能实现(完整代码)
5.1 页面状态与数据加载
在主页面 Index.ets 中,使用 @State 管理数据,aboutToAppear 中调用服务加载数据,并显示加载态。
// pages/Index.ets (部分)
import { UserInfo } from '../model/UserInfo';
import { Vaccine } from '../model/Vaccine';
import { HealthRecord } from '../model/HealthRecord';
import { vaccineService } from '../service/VaccineService';
import { healthRecordService } from '../service/HealthRecordService';
import { userService } from '../service/UserService';
import prompt from '@ohos.prompt';
import router from '@ohos.router';
@Entry
@Component
struct Index {
// ---- 状态变量 ----
@State userInfo: UserInfo = { name: '张三', studentId: '2022001001', college: '计算机学院', avatarColor: '#166534' };
@State vaccines: Vaccine[] = [];
@State records: HealthRecord[] = [];
@State todayChecked: boolean = false;
@State healthCodeStatus: string = '绿码';
@State continuousDays: number = 28;
@State currentTemp: number = 36.5;
@State isLoading: boolean = true;
// 打卡弹窗相关
@State isCheckInSheetVisible: boolean = false;
@State inputTemp: string = '36.5';
@State selectedSymptoms: string[] = [];
@State selectedLocation: string = '学校';
private symptomOptions: string[] = ['发热', '咳嗽', '乏力', '头痛', '腹泻', '无'];
// 疫苗预约对话框
@State isBookDialogVisible: boolean = false;
@State bookingVaccineId: number = -1;
@State bookingDate: string = '';
// 防抖
private debounceTimer: number = -1;
aboutToAppear() {
this.loadAllData();
}
async loadAllData() {
this.isLoading = true;
try {
this.userInfo = await userService.fetch();
this.vaccines = await vaccineService.fetch();
this.records = await healthRecordService.fetch();
this.checkTodayStatus();
this.calcContinuousDays();
this.updateHealthCode();
} catch (e) {
prompt.showToast({ message: '数据加载失败,请重试' });
} finally {
this.isLoading = false;
}
}
// 检查今日是否已打卡
private checkTodayStatus(): void {
const today = new Date().toISOString().slice(0, 10);
this.todayChecked = this.records.some(r => r.date === today);
}
// 计算连续打卡天数(简化:从今天往前推,若中断则重置)
private calcContinuousDays(): void {
// 实际需按日期连续性计算,这里用模拟值,每次打卡成功加1
// 但持久化后可通过服务计算,此处略
}
// 更新健康码状态(根据最近体温和健康状态)
private updateHealthCode(): void {
if (this.records.length === 0) {
this.healthCodeStatus = '绿码';
return;
}
const latest = this.records[0];
if (latest.temperature >= 37.3 || latest.healthStatus === '异常') {
this.healthCodeStatus = '黄码';
} else {
this.healthCodeStatus = '绿码';
}
}
// ... 后续方法
}
5.2 每日健康打卡(含底部弹窗)
点击打卡按钮时,显示底部动作面板(bindSheet),用户填写信息后提交。
// 打开打卡面板
private openCheckInSheet(): void {
if (this.todayChecked) {
prompt.showToast({ message: '今日已打卡,明天再来吧!' });
return;
}
// 重置表单
this.inputTemp = '36.5';
this.selectedSymptoms = [];
this.selectedLocation = '学校';
this.isCheckInSheetVisible = true;
}
// 提交打卡(带防抖)
private submitCheckIn(): void {
if (this.debounceTimer !== -1) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.doCheckIn();
}, 300);
}
private async doCheckIn(): Promise<void> {
const temp = parseFloat(this.inputTemp);
if (isNaN(temp) || temp < 35 || temp > 42) {
prompt.showToast({ message: '请输入有效体温(35~42)' });
return;
}
// 判断健康状态
const status: '健康' | '异常' = temp >= 37.3 ? '异常' : '健康';
const record: HealthRecord = {
id: Date.now(), // 简易唯一id
date: new Date().toISOString().slice(0, 10),
temperature: temp,
healthStatus: status,
location: this.selectedLocation as any,
isReported: true,
symptoms: this.selectedSymptoms
};
// 保存到服务
try {
this.records = await healthRecordService.add(record);
this.todayChecked = true;
this.currentTemp = temp;
// 更新连续天数(若正常打卡)
if (status === '健康') {
this.continuousDays += 1;
}
// 更新健康码(带动画)
animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
this.healthCodeStatus = status === '异常' ? '黄码' : '绿码';
});
// 提示
prompt.showToast({ message: '打卡成功!' });
// 异常预警
if (status === '异常') {
this.triggerAlert();
}
this.isCheckInSheetVisible = false;
} catch (e) {
prompt.showToast({ message: '打卡失败,请重试' });
}
}
// 异常预警:横幅 + 震动
private triggerAlert(): void {
// 通过状态控制横幅显示
this.showAlertBanner = true;
// 震动
try {
vibrator.vibrate(500); // 需导入 @ohos.vibrator
} catch (e) {}
setTimeout(() => { this.showAlertBanner = false; }, 5000);
}
打卡面板的 UI 在 build 中使用 bindSheet:
// 在 build 的根 Column 中添加
.bindSheet($$this.isCheckInSheetVisible, this.CheckInSheetContent(), {
height: 400,
drag: true,
title: { content: '今日健康打卡' }
})
CheckInSheetContent 构建器:
@Builder CheckInSheetContent() {
Column() {
// 体温输入
Row() {
Text('体温(℃)').width(70);
TextInput({ text: this.inputTemp, placeholder: '36.0~37.2' })
.type(InputType.Number)
.width(150)
.onChange(val => this.inputTemp = val);
}.margin(10);
// 症状多选
Text('症状(多选)').alignSelf(ItemAlign.Start).margin({ left: 10, top: 10 });
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.symptomOptions, (item: string) => {
Row() {
Checkbox({ name: item })
.select(this.selectedSymptoms.includes(item))
.onChange((val) => {
if (val) {
this.selectedSymptoms.push(item);
} else {
this.selectedSymptoms = this.selectedSymptoms.filter(s => s !== item);
}
});
Text(item).margin({ left: 5 });
}.margin(5);
});
}.width('100%').padding(10);
// 位置选择
Row() {
Text('位置').width(70);
Radio({ value: '学校', group: 'location' })
.checked(this.selectedLocation === '学校')
.onChange(() => this.selectedLocation = '学校');
Text('学校').margin({ right: 15 });
Radio({ value: '校外', group: 'location' })
.checked(this.selectedLocation === '校外')
.onChange(() => this.selectedLocation = '校外');
Text('校外');
}.margin(10);
Button('提交打卡')
.width('80%')
.height(44)
.backgroundColor('#166534')
.onClick(() => this.submitCheckIn())
.margin({ top: 20 });
}
.padding(16)
.width('100%');
}
5.3 疫苗接种预约(含对话框)
在时间线中,未接种疫苗显示“立即预约”按钮,点击后弹出日期选择对话框。
private showBookDialog(vaccineId: number): void {
this.bookingVaccineId = vaccineId;
// 默认日期为明天
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
this.bookingDate = tomorrow.toISOString().slice(0, 10);
this.isBookDialogVisible = true;
}
private async confirmBooking(): Promise<void> {
if (this.bookingVaccineId === -1) return;
const vaccine = this.vaccines.find(v => v.id === this.bookingVaccineId);
if (!vaccine) return;
if (vaccine.status === '已接种' || vaccine.status === '已预约') {
prompt.showToast({ message: '该疫苗无法预约' });
return;
}
// 更新疫苗状态
const updated: Vaccine = {
...vaccine,
status: '已预约',
inoculateTime: this.bookingDate,
doctor: '王医生(待确认)'
};
try {
this.vaccines = await vaccineService.update(vaccine.id, updated);
prompt.showToast({ message: '预约成功!' });
this.isBookDialogVisible = false;
} catch (e) {
prompt.showToast({ message: '预约失败,请重试' });
}
}
对话框构建:
// 在 build 中添加
.dialog($$this.isBookDialogVisible, this.BookDialogContent())
@Builder BookDialogContent() {
Column() {
Text('预约疫苗').fontSize(18).fontWeight(FontWeight.Bold).margin(12);
Text(`疫苗:${this.vaccines.find(v => v.id === this.bookingVaccineId)?.name || ''}`)
DatePicker({
start: new Date('2026-01-01'),
end: new Date('2027-12-31'),
selected: new Date(this.bookingDate)
})
.onChange((value) => {
this.bookingDate = value.year + '-' + ('0' + (value.month + 1)).slice(-2) + '-' + ('0' + value.day).slice(-2);
})
.margin(12);
Row() {
Button('取消').onClick(() => this.isBookDialogVisible = false).backgroundColor('#999');
Button('确认').onClick(() => this.confirmBooking()).backgroundColor('#166534').margin({ left: 20 });
}
}
.padding(20)
.width('80%')
.backgroundColor('#FFF')
.borderRadius(16);
}
5.4 体温上报与异常预警(含横幅通知)
我们已经在打卡逻辑中集成了异常预警。此外,如果用户单独查看体温记录,可手动触发预警测试。但核心预警已在打卡时自动完成。
横幅通知的 UI 实现:
@State showAlertBanner: boolean = false;
// 在 build 的主 Column 最上方添加
if (this.showAlertBanner) {
Row() {
Image($r('app.media.ic_warning')).width(24).height(24);
Text('⚠️ 体温异常,请及时就医!')
.fontColor('#FFF')
.fontSize(14)
.margin({ left: 8 });
}
.width('100%')
.padding(12)
.backgroundColor('#DC2626')
.animation({ duration: 300 })
}
5.5 健康数据统计(含体温趋势图)
在体温记录区域上方添加折线图,使用 @ohos.arkui.advanced.Chart。
import { Chart, ChartType } from '@ohos.arkui.advanced';
// 在 TemperatureRecords 构建器中添加
@Builder TemperatureRecords() {
Column() {
// ... 标题和统计数字
// 折线图(近7天)
if (this.records.length > 0) {
const recent = this.records.slice(0, 7).reverse(); // 从旧到新
const labels = recent.map(r => r.date.slice(5)); // MM-DD
const data = recent.map(r => r.temperature);
Chart({
type: ChartType.Line,
datasets: [{
data: data,
color: '#166534',
strokeWidth: 2,
pointStyle: { shape: 'circle', size: 4 }
}],
options: {
xAxis: { labels: labels, color: '#999' },
yAxis: { min: 35, max: 38, step: 1, color: '#999' }
}
}).width('100%').height(120).margin({ top: 8 });
}
// 原有列表
List() { ... }
}
}
5.6 数据持久化(Preferences)
已在服务层中实现,BaseService 使用 preferences 保存和加载数据。这样每次启动应用,数据都从本地恢复,不会丢失。
六、UI 界面设计与实现(完整组件)
6.1 顶部健康码卡片(含头像与渐变动画)
@Builder HealthCodeCard() {
Column() {
Row() {
// 头像
Circle({ width: 48, height: 48 })
.fill(this.userInfo.avatarColor)
.overlay(Text(this.userInfo.name.charAt(0)).fontSize(20).fontColor('#FFF'));
Column() {
Text(this.userInfo.name).fontSize(16).fontColor('#FFF');
Text(this.userInfo.studentId).fontSize(12).fontColor('#BBF7D0');
}
.margin({ left: 12 })
.alignItems(HorizontalAlign.Start);
Blank();
// 健康码状态
Row() {
Circle({ width: 12, height: 12 }).fill('#FFF');
Text(this.healthCodeStatus)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFF')
.margin({ left: 6 });
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('rgba(255,255,255,0.2)')
.borderRadius(20);
}
.width('100%');
// 副标题
Text('健康状态:' + (this.healthCodeStatus === '绿码' ? '正常' : '异常,请关注'))
.fontSize(13)
.fontColor('#BBF7D0')
.margin({ top: 8 })
.width('100%');
// 学院信息
Text(`学院:${this.userInfo.college}`)
.fontSize(12)
.fontColor('#BBF7D0')
.margin({ top: 4 })
.width('100%');
}
.width('90%')
.padding(16)
.backgroundColor(this.healthCodeStatus === '绿码' ? '#166534' : '#CA8A04')
.borderRadius(16)
.margin({ top: 12 })
.animation({ duration: 500, curve: Curve.EaseInOut })
}
6.2 健康打卡大按钮(含呼吸动画)
@Builder CheckInButton() {
Column() {
Button(this.todayChecked ? '✅ 今日已打卡' : '今日健康打卡')
.width('100%')
.height(56)
.fontSize(17)
.fontWeight(FontWeight.Medium)
.backgroundColor(this.todayChecked ? '#D1D5DB' : '#166534')
.borderRadius(12)
.enabled(!this.todayChecked)
.onClick(() => this.openCheckInSheet())
.scale(this.todayChecked ? 1 : (this.isLoading ? 1 : 1.02)) // 呼吸效果由外部动画实现
.animation({
duration: 1200,
iterations: -1,
curve: Curve.EaseInOut,
playMode: PlayMode.Alternate
})
.stateStyles({
normal: { scale: 1 },
pressed: { scale: 0.95 }
});
Text(`已连续打卡 ${this.continuousDays} 天`)
.fontSize(14)
.fontColor('#166534')
.margin({ top: 8 });
// 快捷体温
Row() {
Text(`今日体温:${this.currentTemp}℃`)
.fontSize(13)
.fontColor(this.currentTemp >= 37.3 ? '#DC2626' : '#166534');
Text(this.currentTemp >= 37.3 ? '⚠️异常' : '正常')
.fontSize(12)
.fontColor('#FFF')
.backgroundColor(this.currentTemp >= 37.3 ? '#DC2626' : '#166534')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(10)
.margin({ left: 8 });
}
.margin({ top: 6 });
}
.width('90%')
.margin({ top: 12 })
}
6.3 疫苗接种时间线(含展开详情与取消预约)
我们增强时间线:点击节点可展开详情,已预约的疫苗提供“取消预约”按钮。
@Builder VaccineTimeline() {
Column() {
Row() {
Text('疫苗接种记录')
.fontSize(16)
.fontWeight(FontWeight.Medium);
Text(`已接种${this.vaccines.filter(v => v.status === '已接种').length}剂`)
.fontSize(12)
.fontColor('#166534')
.margin({ left: 'auto' });
}
.width('100%')
.margin({ bottom: 12 });
List() {
ForEach(this.vaccines, (vaccine: Vaccine, index: number) => {
ListItem() {
Column() {
Row() {
// 左侧节点
Column() {
Circle({ width: 14, height: 14 })
.fill(vaccine.status === '已接种' ? '#166534' : vaccine.status === '已预约' ? '#16A34A' : '#D1D5DB')
.stroke(vaccine.status === '已预约' ? '#16A34A' : 'transparent')
.strokeWidth(vaccine.status === '已预约' ? 2 : 0);
if (index < this.vaccines.length - 1) {
Rect({ width: 2, height: 50 }).fill('#E5E7EB').margin({ top: 4 });
}
}
.width(20)
.alignItems(HorizontalAlign.Center);
// 右侧内容
Column() {
Row() {
Text(`${vaccine.name} 第${vaccine.dose}剂`)
.fontSize(15)
.fontWeight(vaccine.status === '已接种' ? FontWeight.Medium : FontWeight.Normal)
.fontColor(vaccine.status === '未接种' ? '#999' : '#333');
if (vaccine.status === '已接种') {
Text('✓').fontSize(14).fontColor('#166534').margin({ left: 6 });
} else if (vaccine.status === '已预约') {
Text('⏳').fontSize(14).margin({ left: 6 });
}
}
.width('100%');
Text(vaccine.status === '已接种'
? `${vaccine.factory} · ${vaccine.inoculateTime} · ${vaccine.location}`
: vaccine.nextDueTime)
.fontSize(12)
.fontColor('#999')
.margin({ top: 2 })
.width('100%');
// 操作按钮
Row() {
if (vaccine.status === '未接种') {
Button('立即预约')
.width(76).height(28).fontSize(12)
.backgroundColor('#166534')
.onClick(() => this.showBookDialog(vaccine.id));
} else if (vaccine.status === '已预约') {
Button('取消预约')
.width(76).height(28).fontSize(12)
.backgroundColor('#DC2626')
.onClick(() => {
prompt.showDialog({
title: '确认取消',
message: `确定取消 ${vaccine.name} 的预约吗?`,
buttons: [
{ text: '取消', color: '#999' },
{ text: '确定', color: '#DC2626' }
]
}).then(result => {
if (result.index === 1) {
this.cancelBooking(vaccine.id);
}
});
});
}
// 可添加更多操作
}
.margin({ top: 6 });
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 });
}
.width('100%')
.padding({ top: 6, bottom: 6 })
.onClick(() => {
// 点击可弹出详情,此处略
});
}
}
});
}
.width('100%')
.divider({ strokeWidth: 1, color: '#F0F0F0' })
}
.width('90%')
.padding(16)
.backgroundColor('#FFF')
.borderRadius(12)
.margin({ top: 12 })
}
取消预约实现:
private async cancelBooking(vaccineId: number): Promise<void> {
try {
const list = await vaccineService.fetch();
const target = list.find(v => v.id === vaccineId);
if (target && target.status === '已预约') {
const updated: Vaccine = { ...target, status: '未接种', inoculateTime: '', doctor: '' };
this.vaccines = await vaccineService.update(vaccineId, updated);
prompt.showToast({ message: '已取消预约' });
}
} catch (e) {
prompt.showToast({ message: '取消失败' });
}
}
6.4 体温记录列表(含分页加载)
当记录超过10条时,列表底部显示“加载更多”,触发 onReachEnd。
@State pageSize: number = 10;
@State currentPage: number = 1;
@State displayRecords: HealthRecord[] = [];
// 在 aboutToAppear 中初始化显示
this.updateDisplayRecords();
private updateDisplayRecords(): void {
const start = 0;
const end = this.currentPage * this.pageSize;
this.displayRecords = this.records.slice(start, end);
}
private loadMore(): void {
if (this.displayRecords.length >= this.records.length) {
prompt.showToast({ message: '已加载全部' });
return;
}
this.currentPage++;
this.updateDisplayRecords();
}
// 在 List 中使用
List() {
ForEach(this.displayRecords, (record: HealthRecord) => {
ListItem() { /* 列表项 */ }
})
// 加载更多提示
if (this.displayRecords.length < this.records.length) {
ListItem() {
Text('加载更多...')
.fontSize(12)
.fontColor('#999')
.width('100%')
.textAlign(TextAlign.Center)
.onClick(() => this.loadMore());
}
}
}
.onReachEnd(() => {
if (this.displayRecords.length < this.records.length) {
this.loadMore();
}
})
6.5 底部快捷入口(含路由跳转)
点击各入口通过 router.pushUrl 跳转到对应的页面(此处仅模拟,实际需创建页面)。
@Builder QuickEntryRow() {
Row() {
ForEach([
{ icon: '🏥', name: '校医院预约', route: 'pages/Hospital' },
{ icon: '🧪', name: '核酸查询', route: 'pages/Nucleic' },
{ icon: '📋', name: '就医记录', route: 'pages/Medical' },
{ icon: '💊', name: '用药提醒', route: 'pages/Medicine' }
], (item) => {
Column() {
Text(item.icon).fontSize(28);
Text(item.name).fontSize(12).fontColor('#333').margin({ top: 4 });
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.onClick(() => {
router.pushUrl({ url: `pages/${item.route}` })
.catch(() => {
prompt.showToast({ message: '该功能开发中' });
});
});
});
}
.width('90%')
.backgroundColor('#FFF')
.borderRadius(12)
.margin({ top: 12, bottom: 16 })
.padding({ top: 8, bottom: 8 })
}
七、完整主页面代码(Index.ets)
由于篇幅,此处仅给出完整骨架,实际项目包含所有上述构建器。可参考以下结构(已包含所有关键部分):
// 导入所有必要模块
import { UserInfo } from '../model/UserInfo';
import { Vaccine } from '../model/Vaccine';
import { HealthRecord } from '../model/HealthRecord';
import { vaccineService } from '../service/VaccineService';
import { healthRecordService } from '../service/HealthRecordService';
import { userService } from '../service/UserService';
import prompt from '@ohos.prompt';
import router from '@ohos.router';
import vibrator from '@ohos.vibrator';
import { Chart, ChartType } from '@ohos.arkui.advanced';
@Entry
@Component
struct Index {
// 所有状态变量...
// 所有方法...
// 所有Builder...
build() {
Column() {
if (this.isLoading) {
LoadingProgress().width(60).height(60).color('#166534');
} else {
// 横幅预警
if (this.showAlertBanner) { /* ... */ }
// 健康码
this.HealthCodeCard();
// 打卡按钮
this.CheckInButton();
// 疫苗时间线
this.VaccineTimeline();
// 体温记录(含图表)
this.TemperatureRecords();
// 快捷入口
this.QuickEntryRow();
}
}
.width('100%')
.height('100%')
.backgroundColor('#F0FDF4')
.bindSheet($$this.isCheckInSheetVisible, this.CheckInSheetContent(), {
height: 400,
drag: true,
title: { content: '今日健康打卡' }
})
.dialog($$this.isBookDialogVisible, this.BookDialogContent())
}
}
注意:实际开发时,需要补充缺失的导入和模型文件,并且
dialog和bindSheet的 API 用法以官方文档为准(此处为 ArkTS 声明式语法)。
八、运行与调试
8.1 环境配置
-
确保 DevEco Studio 5.0 以上,SDK API 24。
-
在
oh-package.json5中无需额外依赖(Chart 组件已内置)。 -
为使用
vibrator,需在module.json5中申请权限:"requestPermissions": [ { "name": "ohos.permission.VIBRATE" } ]

8.2 运行步骤
- 导入项目,同步。
- 创建模拟器(API 24)或连接真机。
- 点击运行,观察应用启动。
- 可尝试打卡、预约、查看图表等操作。
8.3 调试技巧
- 使用
HiLog打印日志,分类过滤。 - 使用 DevTools 查看 UI 组件树和状态变化。
- 数据持久化可在 Preferences 文件中查看(通过 DevEco Studio 的 Device File Explorer)。
8.4 常见问题
| 问题 | 解决方案 |
|---|---|
| Chart 组件不显示 | 检查是否导入正确,且数据长度>0 |
| 打卡后连续天数不更新 | 检查计算逻辑,确保每次打卡成功调用 calcContinuousDays |
| 页面刷新数据丢失 | 确认服务层 flush() 被调用 |
| 路由跳转失败 | 确保目标页面在 pages 目录下并在 module.json5 中注册 |
九、项目总结与扩展思路
9.1 项目总结
通过本次实战,我们不仅实现了一个功能完整的校园健康管理应用,更在以下方面有所突破:
- 工程化:分层设计(Service/Model/Page),便于维护和对接后端。
- 交互丰富:弹窗、动画、图表、震动反馈,提升用户体验。
- 数据持久化:使用 Preferences 保存数据,实现本地存储。
- 代码复用:自定义 Builder 构建 UI,提高可读性。
9.2 扩展方向
- 接入真实后端:将 Service 层改造为 HTTP 请求(使用
@ohos.net.http)。 - 推送提醒:使用
@ohos.notification,在疫苗到期前发送通知。 - 多端适配:适配折叠屏、平板,使用
@ohos.mediaquery实现响应式。 - 健康数据共享:对接华为运动健康服务,获取心率、睡眠等数据。
- AI 辅助诊断:结合症状和体温,给出初步就医建议(接入 AI 接口)。
9.3 结语
健康管理是智慧校园的重要组成部分,本文提供的完整代码框架可快速迁移到其他健康类场景。希望读者不仅能运行起来,更能深入理解 ArkTS 的状态管理和声明式 UI 开发。如果本文对您有帮助,欢迎点赞收藏,也期待您的实践反馈!
更多推荐



所有评论(0)