项目效果

本文实现的是一个基于 HarmonyOS 和 ArkTS 的校园自习室座位预约应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理座位数据,实现自习室分区切换、座位状态展示、座位预约、预约取消、预约统计和当前预约信息展示等功能。

最终运行效果如下:

【在这里插入项目运行截图】

页面主要包含以下内容:

  • 顶部应用标题;
  • 自习室总座位统计;
  • 已预约座位统计;
  • 可用座位统计;
  • 自习室区域切换;
  • 预约人姓名输入;
  • 预约时间选择;
  • 座位列表展示;
  • 座位状态区分;
  • 点击选择可用座位;
  • 确认预约座位;
  • 取消已有预约;
  • 清空当前区域预约;
  • 当前预约信息展示;
  • 页面整体采用 ArkUI 声明式布局。

本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个完整的校园场景工具应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。

为了避免文章检测时出现链接占比过高的问题,本文不直接放图片链接。发布时可以在 CSDN 编辑器中手动插入一张应用运行总效果图。


前言

在校园生活中,自习室座位预约是一个很常见的场景。尤其是考试周、期末复习阶段,学生需要提前查看座位是否空闲,并选择合适的座位进行学习。如果没有预约和状态展示功能,用户只能到现场查看,效率比较低。

从应用开发角度来看,自习室座位预约应用非常适合作为 HarmonyOS ArkTS 的练习项目。它不依赖后端接口,也不需要复杂数据库,但可以完整练习页面布局、输入绑定、状态管理、列表渲染、条件筛选、按钮交互和数据统计。

本文基于 HarmonyOS 和 ArkTS 实现一个轻量级校园自习室座位预约应用。用户可以切换不同自习区域,查看每个座位的状态,输入预约人姓名和预约时间后选择座位完成预约。对于已经预约的座位,用户也可以取消预约。

这个项目的核心不是简单展示一个座位列表,而是通过状态数据驱动页面变化。座位是否可用、预约人是谁、预约时间是什么、当前区域还有多少空位,这些内容都来自座位数组。当数组发生变化时,页面会自动刷新。


一、项目目标

本次实践主要实现以下目标:

  • 创建 HarmonyOS ArkTS 页面;
  • 使用 @Entry@Component 定义页面组件;
  • 使用 @State 管理页面状态;
  • 使用 TextInput 接收预约人姓名;
  • 使用按钮切换预约时间;
  • 使用按钮切换自习室区域;
  • 使用数组保存座位数据;
  • 使用 ListForEach 渲染座位列表;
  • 根据区域筛选当前座位;
  • 根据座位状态区分可预约和已预约;
  • 支持点击选择可用座位;
  • 支持确认预约座位;
  • 支持取消单个座位预约;
  • 支持清空当前区域预约;
  • 根据座位数组动态统计总数、已预约数和可用数;
  • 使用 @Builder 封装统计卡片、区域按钮、时间按钮和座位卡片;
  • 完成一个可以运行的校园自习室预约页面。

这个项目虽然是单页面应用,但功能链路比较完整,包含输入、选择、预约、取消、筛选、统计和状态展示等流程,比单纯的静态页面更适合作为 ArkTS 综合练习。


二、技术栈

类型 内容
开发方向 HarmonyOS 应用开发
开发语言 ArkTS
UI 框架 ArkUI
SDK 版本 HarmonyOS API 23 及以上
工程模型 Stage 模型
核心组件 Text / TextInput / Button / Column / Row / List / ForEach
状态管理 @State
数据处理 数组遍历 / map / filter / 条件判断
项目入口 entry/src/main/ets/pages/Index.ets
运行平台 模拟器或真机

本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。


三、为什么选择自习室座位预约项目

校园自习室座位预约应用适合作为 ArkTS 练习项目,主要有以下几个原因。

第一,业务场景真实。学生在备考、写作业、做项目时经常需要找自习室座位。座位是否可用、在哪个区域、是否已经被预约,都是实际会遇到的问题。

第二,交互流程完整。用户需要切换自习区域、输入预约人姓名、选择预约时间、选择座位、确认预约、取消预约。这个流程比普通展示页面更接近真实应用。

第三,适合练习数组操作。每个座位都是数组中的一个对象。预约座位需要更新数组中的某一项,取消预约也需要更新对应座位状态,统计数量需要遍历数组。

第四,适合理解状态管理。当前区域、预约人姓名、预约时间、选中座位和座位列表都属于页面状态。状态发生变化后,页面会自动重新渲染。

第五,容易扩展。基础功能完成后,可以继续增加本地存储、预约日期、座位平面图、扫码签到、违规释放座位等功能。

在本项目中,seats 是最核心的数据源。页面中的座位列表、可用数量、预约数量和当前预约信息都由它计算或渲染得到。这样可以保证页面展示和数据状态保持一致。


四、功能规则说明

本文实现的自习室预约应用包含三个区域:

区域 说明
A区 靠窗安静区
B区 普通学习区
C区 小组讨论区

座位状态分为两类:

状态 含义
可预约 当前座位暂时无人预约
已预约 当前座位已经被预约

预约流程如下:

选择区域 → 输入姓名 → 选择时间 → 选择可用座位 → 点击确认预约

取消预约流程如下:

找到已预约座位 → 点击取消预约 → 座位恢复为可预约状态

统计规则如下:

总座位数 = 当前系统中所有座位数量
已预约数 = 状态为“已预约”的座位数量
可用座位数 = 状态为“可预约”的座位数量

当前区域切换后,页面只显示该区域下的座位,但顶部统计仍然展示整体座位情况。


五、项目结构

本项目主要修改首页文件:

entry
└── src
    └── main
        └── ets
            └── pages
                └── Index.ets

其中:

文件 作用
Index.ets 编写页面结构、状态数据和座位预约逻辑

本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。

如果后续继续扩展,可以考虑把座位卡片、统计卡片、区域切换按钮和预约表单拆分成独立组件。


六、核心实现思路

本项目的核心流程如下:

  1. 定义座位数据结构;
  2. 使用 @State 保存预约人姓名、当前区域、预约时间、选中座位和座位数组;
  3. 用户切换自习室区域;
  4. 用户输入预约人姓名;
  5. 用户选择预约时间段;
  6. 用户点击可预约座位;
  7. 点击确认预约后更新对应座位状态;
  8. 座位状态从“可预约”变为“已预约”;
  9. 页面自动更新座位列表和统计数据;
  10. 点击取消预约后恢复座位状态;
  11. 点击清空当前区域预约后释放当前区域全部座位;
  12. 当没有选中座位时给出默认提示。

项目中最重要的状态变量如下:

@State userName: string = ''
@State selectedArea: string = 'A区'
@State selectedTime: string = '08:00-10:00'
@State selectedSeatId: number = -1
@State seats: SeatItem[] = []

其中:

状态变量 作用
userName 保存预约人姓名
selectedArea 保存当前自习室区域
selectedTime 保存当前预约时间段
selectedSeatId 保存当前选中的座位编号
seats 保存全部座位数据

seats 是本项目中最重要的数据。座位列表、预约状态和统计数据都依赖这个数组。


七、Index.ets 完整代码

打开文件:

entry/src/main/ets/pages/Index.ets

将其中内容替换为下面代码:

interface SeatItem {
  id: number
  name: string
  area: string
  status: string
  user: string
  time: string
}

@Entry
@Component
struct Index {
  @State userName: string = ''
  @State selectedArea: string = 'A区'
  @State selectedTime: string = '08:00-10:00'
  @State selectedSeatId: number = -1

  @State seats: SeatItem[] = [
    {
      id: 1,
      name: 'A-01',
      area: 'A区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 2,
      name: 'A-02',
      area: 'A区',
      status: '已预约',
      user: '张同学',
      time: '10:00-12:00'
    },
    {
      id: 3,
      name: 'A-03',
      area: 'A区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 4,
      name: 'A-04',
      area: 'A区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 5,
      name: 'B-01',
      area: 'B区',
      status: '已预约',
      user: '李同学',
      time: '14:00-16:00'
    },
    {
      id: 6,
      name: 'B-02',
      area: 'B区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 7,
      name: 'B-03',
      area: 'B区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 8,
      name: 'B-04',
      area: 'B区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 9,
      name: 'C-01',
      area: 'C区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 10,
      name: 'C-02',
      area: 'C区',
      status: '已预约',
      user: '王同学',
      time: '18:00-20:00'
    },
    {
      id: 11,
      name: 'C-03',
      area: 'C区',
      status: '可预约',
      user: '',
      time: ''
    },
    {
      id: 12,
      name: 'C-04',
      area: 'C区',
      status: '可预约',
      user: '',
      time: ''
    }
  ]

  private getAreaSeats(): SeatItem[] {
    return this.seats.filter((item: SeatItem) => item.area === this.selectedArea)
  }

  private getReservedCount(): number {
    return this.seats.filter((item: SeatItem) => item.status === '已预约').length
  }

  private getAvailableCount(): number {
    return this.seats.filter((item: SeatItem) => item.status === '可预约').length
  }

  private getSelectedSeatName(): string {
    let result: string = '暂未选择座位'

    this.seats.forEach((item: SeatItem) => {
      if (item.id === this.selectedSeatId) {
        result = item.name
      }
    })

    return result
  }

  private selectSeat(item: SeatItem): void {
    if (item.status === '已预约') {
      return
    }

    this.selectedSeatId = item.id
  }

  private reserveSeat(): void {
    let name: string = this.userName.trim()

    if (name.length === 0 || this.selectedSeatId === -1) {
      return
    }

    this.seats = this.seats.map((item: SeatItem) => {
      if (item.id === this.selectedSeatId && item.status === '可预约') {
        return {
          id: item.id,
          name: item.name,
          area: item.area,
          status: '已预约',
          user: name,
          time: this.selectedTime
        }
      }

      return item
    })

    this.userName = ''
    this.selectedSeatId = -1
  }

  private cancelSeat(id: number): void {
    this.seats = this.seats.map((item: SeatItem) => {
      if (item.id === id) {
        return {
          id: item.id,
          name: item.name,
          area: item.area,
          status: '可预约',
          user: '',
          time: ''
        }
      }

      return item
    })

    if (this.selectedSeatId === id) {
      this.selectedSeatId = -1
    }
  }

  private clearCurrentArea(): void {
    this.seats = this.seats.map((item: SeatItem) => {
      if (item.area === this.selectedArea) {
        return {
          id: item.id,
          name: item.name,
          area: item.area,
          status: '可预约',
          user: '',
          time: ''
        }
      }

      return item
    })

    this.selectedSeatId = -1
  }

  private getStatusColor(status: string): string {
    if (status === '已预约') {
      return '#EF4444'
    }

    return '#10B981'
  }

  private getAreaTip(): string {
    if (this.selectedArea === 'A区') {
      return '靠窗安静区,适合长时间自习。'
    }

    if (this.selectedArea === 'B区') {
      return '普通学习区,适合日常复习。'
    }

    return '小组讨论区,适合多人协作学习。'
  }

  @Builder
  StatCard(title: string, value: string, color: string) {
    Column() {
      Text(value)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)

      Text(title)
        .fontSize(13)
        .fontColor('#6B7280')
        .margin({ top: 4 })
    }
    .width('31%')
    .height(78)
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .shadow({
      radius: 10,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  @Builder
  AreaButton(text: string) {
    Button(text)
      .height(36)
      .layoutWeight(1)
      .fontSize(14)
      .fontColor(this.selectedArea === text ? Color.White : '#4B5563')
      .backgroundColor(this.selectedArea === text ? '#0A59F7' : '#EEF2F8')
      .borderRadius(18)
      .onClick(() => {
        this.selectedArea = text
        this.selectedSeatId = -1
      })
  }

  @Builder
  TimeButton(text: string) {
    Button(text)
      .height(34)
      .fontSize(13)
      .fontColor(this.selectedTime === text ? Color.White : '#4B5563')
      .backgroundColor(this.selectedTime === text ? '#0A59F7' : '#EEF2F8')
      .borderRadius(17)
      .onClick(() => {
        this.selectedTime = text
      })
  }

  @Builder
  SeatCard(item: SeatItem) {
    Column() {
      Row() {
        Column() {
          Text(item.name)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#182431')

          Text(`${item.area} · ${item.status}`)
            .fontSize(13)
            .fontColor(this.getStatusColor(item.status))
            .margin({ top: 6 })

          if (item.status === '已预约') {
            Text(`${item.user}  ${item.time}`)
              .fontSize(12)
              .fontColor('#9CA3AF')
              .margin({ top: 6 })
          } else {
            Text('当前座位可以预约')
              .fontSize(12)
              .fontColor('#9CA3AF')
              .margin({ top: 6 })
          }
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        if (item.status === '可预约') {
          Button(this.selectedSeatId === item.id ? '已选择' : '选择')
            .height(34)
            .fontSize(13)
            .fontColor(this.selectedSeatId === item.id ? Color.White : '#0A59F7')
            .backgroundColor(this.selectedSeatId === item.id ? '#0A59F7' : '#EEF2F8')
            .borderRadius(16)
            .onClick(() => {
              this.selectSeat(item)
            })
        } else {
          Button('取消')
            .height(34)
            .fontSize(13)
            .fontColor('#EF4444')
            .backgroundColor('#FEF2F2')
            .borderRadius(16)
            .onClick(() => {
              this.cancelSeat(item.id)
            })
        }
      }
      .width('100%')
    }
    .width('100%')
    .padding(14)
    .margin({ bottom: 12 })
    .backgroundColor(this.selectedSeatId === item.id ? '#EEF6FF' : Color.White)
    .borderRadius(16)
    .shadow({
      radius: 10,
      color: '#12000000',
      offsetX: 0,
      offsetY: 3
    })
  }

  build() {
    Column() {
      Text('校园自习室预约')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#182431')
        .margin({ top: 22 })

      Text('基于 HarmonyOS ArkTS 的轻量级座位管理工具')
        .fontSize(14)
        .fontColor('#6B7280')
        .margin({ top: 8, bottom: 22 })

      Row() {
        this.StatCard('总座位', `${this.seats.length}`, '#0A59F7')
        this.StatCard('已预约', `${this.getReservedCount()}`, '#EF4444')
        this.StatCard('可预约', `${this.getAvailableCount()}`, '#10B981')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Column() {
        Text('选择自习区域')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 12 })

        Row() {
          this.AreaButton('A区')
          Blank().width(10)
          this.AreaButton('B区')
          Blank().width(10)
          this.AreaButton('C区')
        }
        .width('100%')

        Text(this.getAreaTip())
          .fontSize(13)
          .fontColor('#6B7280')
          .lineHeight(20)
          .margin({ top: 12 })
          .width('100%')
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(18)
      .margin({ top: 18 })
      .shadow({
        radius: 10,
        color: '#12000000',
        offsetX: 0,
        offsetY: 4
      })

      Column() {
        Text('预约信息')
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')
          .width('100%')
          .margin({ bottom: 12 })

        TextInput({
          placeholder: '请输入预约人姓名,例如 张同学',
          text: this.userName
        })
          .height(42)
          .fontSize(14)
          .backgroundColor('#F5F7FA')
          .borderRadius(12)
          .padding({ left: 12, right: 12 })
          .onChange((value: string) => {
            this.userName = value
          })

        Text('选择预约时间')
          .fontSize(14)
          .fontColor('#6B7280')
          .width('100%')
          .margin({ top: 14, bottom: 10 })

        Row() {
          this.TimeButton('08:00-10:00')
          this.TimeButton('10:00-12:00')
          this.TimeButton('14:00-16:00')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          this.TimeButton('16:00-18:00')
          this.TimeButton('18:00-20:00')
        }
        .width('100%')
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })

        Text(`当前选择:${this.getSelectedSeatName()},时间:${this.selectedTime}`)
          .fontSize(13)
          .fontColor('#6B7280')
          .lineHeight(20)
          .margin({ top: 14 })
          .width('100%')

        Button('确认预约')
          .height(44)
          .fontSize(16)
          .fontColor(Color.White)
          .backgroundColor('#0A59F7')
          .borderRadius(14)
          .width('100%')
          .margin({ top: 14 })
          .onClick(() => {
            this.reserveSeat()
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(18)
      .margin({ top: 18 })
      .shadow({
        radius: 10,
        color: '#12000000',
        offsetX: 0,
        offsetY: 4
      })

      Row() {
        Text(`${this.selectedArea}座位列表`)
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')

        Blank()

        Button('清空本区')
          .height(32)
          .fontSize(13)
          .fontColor('#EF4444')
          .backgroundColor('#FEF2F2')
          .borderRadius(14)
          .onClick(() => {
            this.clearCurrentArea()
          })
      }
      .width('100%')
      .margin({ top: 20, bottom: 12 })

      List() {
        ForEach(this.getAreaSeats(), (item: SeatItem) => {
          ListItem() {
            this.SeatCard(item)
          }
        }, (item: SeatItem) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .padding({ left: 18, right: 18 })
    .backgroundColor('#F5F7FA')
  }
}

八、代码实现说明

1. 定义座位数据结构

项目中使用 SeatItem 接口描述每一个座位:

interface SeatItem {
  id: number
  name: string
  area: string
  status: string
  user: string
  time: string
}

字段说明如下:

字段 作用
id 座位唯一编号
name 座位名称
area 所属区域
status 座位状态
user 预约人
time 预约时间

其中 id 用来区分不同座位,area 用来按照区域筛选座位,status 用来判断座位是否可以预约。


2. 使用 @State 管理页面数据

本项目中使用多个状态变量:

@State userName: string = ''
@State selectedArea: string = 'A区'
@State selectedTime: string = '08:00-10:00'
@State selectedSeatId: number = -1
@State seats: SeatItem[] = []

这些状态变量分别保存预约人姓名、当前区域、预约时间、当前选中座位和座位数组。

当用户切换区域、选择座位、输入姓名、确认预约或取消预约时,状态数据都会发生变化,页面会自动重新渲染。


3. 按区域筛选座位

当前区域下的座位通过 getAreaSeats 方法获取:

private getAreaSeats(): SeatItem[] {
  return this.seats.filter((item: SeatItem) => item.area === this.selectedArea)
}

这段代码会从全部座位中筛选出当前区域的座位。

点击 A区、B区、C区按钮时,selectedArea 会发生变化,座位列表也会自动切换。


4. 选择可预约座位

选择座位通过 selectSeat 方法实现:

private selectSeat(item: SeatItem): void {
  if (item.status === '已预约') {
    return
  }

  this.selectedSeatId = item.id
}

如果座位已经被预约,就不能再次选择。只有状态为“可预约”的座位才能被选中。

选中座位后,页面会显示“已选择”状态,预约信息区域也会同步显示当前选择的座位名称。


5. 确认预约座位

确认预约通过 reserveSeat 方法实现:

private reserveSeat(): void {
  let name: string = this.userName.trim()

  if (name.length === 0 || this.selectedSeatId === -1) {
    return
  }

  this.seats = this.seats.map((item: SeatItem) => {
    if (item.id === this.selectedSeatId && item.status === '可预约') {
      return {
        id: item.id,
        name: item.name,
        area: item.area,
        status: '已预约',
        user: name,
        time: this.selectedTime
      }
    }

    return item
  })
}

这里先判断预约人姓名是否为空,同时判断是否已经选择座位。如果没有输入姓名或没有选择座位,就不执行预约。

预约成功后,对应座位的状态会从“可预约”变为“已预约”,并保存预约人姓名和预约时间。


6. 取消座位预约

取消预约通过 cancelSeat 方法实现:

private cancelSeat(id: number): void {
  this.seats = this.seats.map((item: SeatItem) => {
    if (item.id === id) {
      return {
        id: item.id,
        name: item.name,
        area: item.area,
        status: '可预约',
        user: '',
        time: ''
      }
    }

    return item
  })
}

取消后,该座位会重新恢复为可预约状态,预约人和预约时间会被清空。

这样用户可以重新选择这个座位进行预约。


7. 清空当前区域预约

清空当前区域预约通过 clearCurrentArea 方法实现:

private clearCurrentArea(): void {
  this.seats = this.seats.map((item: SeatItem) => {
    if (item.area === this.selectedArea) {
      return {
        id: item.id,
        name: item.name,
        area: item.area,
        status: '可预约',
        user: '',
        time: ''
      }
    }

    return item
  })
}

这段代码只会清空当前区域的预约,不会影响其他区域的座位状态。

例如当前选择 A区,点击“清空本区”后,只有 A区座位会恢复为可预约状态,B区和 C区不受影响。


8. 统计座位数量

页面顶部有三个统计卡片,分别显示总座位、已预约和可预约数量。

已预约数量通过下面的方法计算:

private getReservedCount(): number {
  return this.seats.filter((item: SeatItem) => item.status === '已预约').length
}

可预约数量通过下面的方法计算:

private getAvailableCount(): number {
  return this.seats.filter((item: SeatItem) => item.status === '可预约').length
}

当预约或取消预约后,座位状态发生变化,统计数字也会自动更新。


9. 区域提示信息

不同区域有不同用途,因此项目中通过 getAreaTip 方法返回区域说明:

private getAreaTip(): string {
  if (this.selectedArea === 'A区') {
    return '靠窗安静区,适合长时间自习。'
  }

  if (this.selectedArea === 'B区') {
    return '普通学习区,适合日常复习。'
  }

  return '小组讨论区,适合多人协作学习。'
}

切换区域后,页面会显示对应区域的说明文字,让用户更清楚每个区域的特点。


10. 使用 @Builder 封装页面结构

本项目中使用 @Builder 封装了多个页面片段:

@Builder
StatCard(title: string, value: string, color: string)

用于构建顶部统计卡片。

@Builder
AreaButton(text: string)

用于构建区域切换按钮。

@Builder
TimeButton(text: string)

用于构建预约时间按钮。

@Builder
SeatCard(item: SeatItem)

用于构建座位卡片。

这样可以减少 build() 方法中的代码堆叠,让页面结构更加清晰,也方便后续维护。


九、运行项目

代码编写完成后,在 DevEco Studio 中选择模拟器或真机运行项目。

运行成功后,页面会显示“校园自习室预约”。用户可以切换 A区、B区、C区,查看不同区域的座位状态。输入预约人姓名并选择可用座位后,点击“确认预约”即可完成预约。

可以按照以下步骤进行测试:

  1. 打开应用,检查页面是否正常显示;
  2. 查看总座位、已预约和可预约数量是否正确;
  3. 点击 A区,检查是否显示 A区座位;
  4. 点击 B区,检查是否切换到 B区座位;
  5. 输入预约人姓名,例如 张同学;
  6. 选择预约时间,例如 18:00-20:00;
  7. 选择一个可预约座位;
  8. 点击“确认预约”,检查座位是否变为已预约;
  9. 检查顶部统计数字是否同步变化;
  10. 点击已预约座位的“取消”按钮;
  11. 检查座位是否恢复为可预约;
  12. 点击“清空本区”,检查当前区域预约是否全部清空;
  13. 切换到其他区域,检查其他区域座位是否没有被误清空。

测试结果如下:

测试功能 测试结果
页面正常显示 成功
区域切换 成功
座位状态展示 成功
选择座位 成功
确认预约 成功
取消预约 成功
清空当前区域 成功
总座位统计 成功
已预约统计 成功
可预约统计 成功
预约信息展示 成功

经过测试,应用主要功能可以正常运行,页面状态也能根据用户操作及时刷新。


十、开发中遇到的问题

1. 已预约座位不能再次选择

如果已预约座位还能被选择,就会造成预约冲突。因此在选择座位时需要判断状态:

if (item.status === '已预约') {
  return
}

这样可以保证只有可预约座位才能被选中。


2. 预约前需要校验姓名和座位

如果没有输入预约人姓名,或者没有选择座位,就不应该执行预约。

if (name.length === 0 || this.selectedSeatId === -1) {
  return
}

这样可以避免无效预约进入座位数据。


3. 修改座位状态不能破坏其他字段

预约时需要保留座位编号、座位名称和所属区域,只修改状态、预约人和预约时间。

因此项目中使用 map() 返回新的座位对象,而不是直接写死整个数组。


4. 清空当前区域不能影响其他区域

清空预约时,只应该释放当前区域的座位,其他区域不应该被修改。

if (item.area === this.selectedArea) {
  return {
    id: item.id,
    name: item.name,
    area: item.area,
    status: '可预约',
    user: '',
    time: ''
  }
}

这样可以保证清空操作范围准确。


5. 统计数据不能写死

总座位、已预约和可预约数量都不能写死。因为预约和取消操作会不断改变座位状态。

本项目通过 filter() 动态计算统计结果,保证顶部统计数字始终和座位列表一致。


十一、总结

本文基于 HarmonyOS 和 ArkTS 实现了一个校园自习室座位预约应用。项目通过 @State 管理页面数据,使用 ArkUI 组件完成页面布局,并实现了区域切换、座位状态展示、座位预约、取消预约、清空当前区域预约和座位数量统计等功能。

通过本次实践,主要完成了以下内容:

  • 使用 @Entry@Component 创建页面组件;
  • 使用 @State 保存预约人、区域、时间、选中座位和座位数据;
  • 使用 TextInput 获取预约人姓名;
  • 使用 Button 实现区域切换、时间选择、座位选择和取消预约;
  • 使用 ListForEach 渲染座位列表;
  • 使用 filter() 按区域筛选座位;
  • 使用 map() 更新指定座位状态;
  • 使用状态变量控制页面显示;
  • 使用 @Builder 封装页面局部结构;
  • 完成一个可以运行的 HarmonyOS 校园工具页面。

这个项目虽然是一个单页面练习,但完整展示了 ArkTS 页面开发中的输入处理、状态更新、条件判断、列表渲染和动态统计流程。

后续可以在这个基础上继续扩展,例如:

  • 增加预约日期选择;
  • 增加本地数据持久化;
  • 增加座位平面图展示;
  • 增加座位搜索功能;
  • 增加预约时长限制;
  • 增加扫码签到;
  • 增加超时自动释放座位;
  • 增加我的预约页面;
  • 增加深色模式适配;
  • 增加多自习室管理。

整体来看,校园自习室座位预约应用非常适合作为 HarmonyOS ArkTS 的练习项目。它功能清晰、代码量适中、运行效果直观,能够帮助初学者理解 ArkUI 声明式开发和 @State 状态驱动页面刷新的基本思想。

Logo

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

更多推荐