HarmonyOS ArkTS 实战:从零实现校园疫苗接种与健康管理应用

适用版本:HarmonyOS API 24 + DevEco Studio 5.0


目录


一、项目背景与效果预览

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 核心功能清单(细化至用户故事)

  1. 健康码展示:根据当日体温和打卡状态自动生成绿/黄/红码,动态更新。
  2. 每日健康打卡:用户填写体温、症状(多选)、所在位置,提交后记录打卡历史,并累计连续打卡天数。
  3. 疫苗接种管理:查看所有疫苗的接种状态(已接种/已预约/未接种),预约未接种疫苗(选择日期),取消已预约。
  4. 体温趋势分析:以折线图展示近7天体温变化,同时统计正常率和平均体温。
  5. 异常预警:体温≥37.3℃时,顶部弹出黄色横幅并调用系统震动,提醒用户就医。
  6. 数据持久化:所有打卡记录、疫苗状态保存到 Preferences,退出应用后数据不丢失。
  7. 快捷功能路由:点击“校医院预约”等入口,跳转到独立页面(模拟),展示完整应用架构。

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);
}

打卡面板的 UIbuild 中使用 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())
  }
}

注意:实际开发时,需要补充缺失的导入和模型文件,并且 dialogbindSheet 的 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 运行步骤

  1. 导入项目,同步。
  2. 创建模拟器(API 24)或连接真机。
  3. 点击运行,观察应用启动。
  4. 可尝试打卡、预约、查看图表等操作。

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 扩展方向

  1. 接入真实后端:将 Service 层改造为 HTTP 请求(使用 @ohos.net.http)。
  2. 推送提醒:使用 @ohos.notification,在疫苗到期前发送通知。
  3. 多端适配:适配折叠屏、平板,使用 @ohos.mediaquery 实现响应式。
  4. 健康数据共享:对接华为运动健康服务,获取心率、睡眠等数据。
  5. AI 辅助诊断:结合症状和体温,给出初步就医建议(接入 AI 接口)。

9.3 结语

健康管理是智慧校园的重要组成部分,本文提供的完整代码框架可快速迁移到其他健康类场景。希望读者不仅能运行起来,更能深入理解 ArkTS 的状态管理和声明式 UI 开发。如果本文对您有帮助,欢迎点赞收藏,也期待您的实践反馈!

Logo

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

更多推荐