鸿蒙HarmonyOS ArkTS 实战:教师座椅出入记录 APP 从零到一

API Version : HarmonyOS API 24 (HarmonyOS 4.0+)

语言 : ArkTS(鸿蒙原生声明式 UI 框架)

开发工具 : DevEco Studio

完整源码: 见文章底部


目录

  1. 项目背景与需求分析
  2. 技术选型与架构设计
  3. 项目搭建与配置
  4. 数据模型设计
  5. [UI 组件详解](#UI 组件详解)
  6. 业务逻辑与状态管理
  7. [ArkTS 语法避坑指南](#ArkTS 语法避坑指南)
  8. [UI 布局细节分析](#UI 布局细节分析)
  9. 完整代码解析
  10. 运行效果与演示
  11. 扩展与优化方向
  12. 总结与心得

1. 项目背景与需求分析

1.1 场景痛点

在学校、公司、图书馆等场景中,经常需要记录人员的座位使用情况。以教师办公室为例:

  • 教师出入频繁,领导或同事想知道"张老师现在在不在座位上?"
  • 需要记录教师入座时间离开时间 ,统计在座时长
  • 需要查看历史出入记录,了解一段时间内的办公规律

传统做法是用 Excel 或纸质登记,效率低、易出错、无法实时查看。

1.2 功能需求

我们决定开发一个 教师座椅出入记录 APP,包含以下功能:

功能模块 具体需求 优先级
教师列表展示 显示所有教师头像、姓名、状态 P0
实时状态 显示教师当前是"在座"还是"离座" P0
入座操作 点击"入座"按钮记录入座时间和操作 P0
离开操作 点击"离开"自动计算本次在座时长并累加 P0
累计时长统计 显示每位教师今日在座总时长 P1
出入记录列表 按时间倒序展示所有出入操作记录 P1
在座/离座统计 顶部概览卡片显示在座人数、离座人数 P1
Tab 切换 教师列表视图和记录列表视图切换 P1
重置功能 一键清除所有状态和记录 P2

1.3 技术目标

  • 使用 HarmonyOS API 24 最新 ArkTS 语法
  • 纯声明式 UI 编程范式
  • 深色主题,现代 UI 风格
  • 代码精简、可维护、可扩展

2. 技术选型与架构设计

2.1 为什么选择 ArkTS?

ArkTS 是鸿蒙原生开发语言,基于 TypeScript 但做了精简和强化

  • 声明式 UI :通过 @Component + build() 描述界面,无需 XML 布局文件
  • 响应式状态@State 装饰器让数据和 UI 自动同步
  • 类型安全:强类型系统,编译阶段即可发现大部分错误
  • 高性能:Ark Compiler 直接编译机器码,无 JIT 开销
  • API 24 生态:丰富的 UI 组件(Grid、Scroll、Row、Column、Button 等)

2.2 架构设计

APP 采用 单页面 + 多组件 架构:

复制代码
TeacherSeatRecord (主页面 @Entry)
  ├── TeacherCard (教师卡片子组件)
  │     ├── 头像(姓氏首字)
  │     ├── 姓名
  │     ├── 状态标签(在座/离座)
  │     ├── 累计时长
  │     └── 操作按钮(入座/离开)
  ├── RecordItem (记录条目子组件)
  │     ├── 类型图标(入座/离开)
  │     ├── 教师姓名 + 操作类型
  │     └── 操作时间
  ├── 顶部标题栏
  ├── 统计卡片(在座数/总数/离座数)
  └── Tab 切换(教师列表 / 出入记录)

2.3 数据流设计

复制代码
用户点击 → handleSit/handleLeave → @State teachers/records 更新 → UI 自动刷新
                                          ↓
                                  累计时长计算
                                          ↓
                                  记录列表追加

ArkTS 的 @State 装饰器确保数据变化后 UI 自动重新渲染,无需手动操作 DOM。


3. 项目搭建与配置

3.1 创建项目

在 DevEco Studio 中创建 Empty Ability 模板项目:

  • Project Type: Application
  • Language: ArkTS
  • Device Type: Phone / Tablet
  • API Version: 9+(兼容 API 24)

3.2 配置页面路由

main_pages.json 配置文件注册所有页面:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/TeacherSeatRecord"
  ]
}

3.3 页面导航

首页 Index.ets 通过 router.pushUrl 跳转到详情页:

typescript 复制代码
Button('💺 教师座椅出入记录')
  .onClick(() => {
    router.pushUrl({ url: 'pages/TeacherSeatRecord' });
  })

这里有个小细节:router.pushUrl 传参时 不需要写 .ets 后缀,系统会自动补全。

3.4 项目文件结构

复制代码
entry/src/main/ets/
  pages/
    Index.ets               # 首页导航
    TeacherSeatRecord.ets   # 教师座椅出入记录主页面
entry/src/main/resources/
  base/
    profile/
      main_pages.json       # 页面路由配置

4. 数据模型设计

4.1 枚举:教师状态

typescript 复制代码
enum SeatStatus {
  AWAY = 'away',    // 离座
  SEATED = 'seated' // 在座
}

为什么用枚举而不是布尔值?因为后续可能扩展为 临时离开会议中 等状态,枚举的可扩展性最好。

4.2 枚举:操作类型

typescript 复制代码
enum RecordType {
  SIT = 'sit',      // 入座
  LEAVE = 'leave'   // 离开
}

SeatStatus 分开定义,职责更清晰。

4.3 接口:教师数据

typescript 复制代码
interface Teacher {
  id: number;
  name: string;
  status: SeatStatus;
  seatedTime?: string;    // 入座时间(可选)
  totalMinutes: number;   // 今日累计在座时长(分钟)
  colorIndex: number;     // 头像颜色索引
}

关键设计点:

  • seatedTime可选属性 ?,表示只有"在座"状态时才有值
  • totalMinutes 用分钟数存储而不是"小时:分钟"字符串,方便计算
  • colorIndex 解耦颜色逻辑,方便扩展不同头像颜色

4.4 接口:出入记录

typescript 复制代码
interface SeatRecord {
  id: number;
  teacherId: number;
  teacherName: string;    // 冗余字段,方便显示
  type: RecordType;       // 入座或离开
  time: string;           // "HH:mm:ss" 格式
  date: string;           // "YYYY-MM-DD" 格式
}

teacherName冗余字段 ,因为记录列表中需要显示教师名称,如果不冗余每次都要从 teacherId 查找,增加复杂度。在 ArkTS 中这种小规模数据(几十条记录)的冗余是合理的。

4.5 常量与模拟数据

typescript 复制代码
const AVATAR_COLORS: string[] = [
  '#4D96FF', '#6BCB77', '#FFA94D', '#FF6B6B',
  '#9B59B6', '#00D2FF', '#FF85A2', '#FFD93D'
];

const TEACHERS_DATA: Teacher[] = [
  { id: 1, name: '张老师', status: SeatStatus.AWAY, totalMinutes: 0, colorIndex: 0 },
  { id: 2, name: '李老师', status: SeatStatus.AWAY, totalMinutes: 0, colorIndex: 1 },
  // ... 共 8 位教师
];

颜色数组有 8 个值,8 位教师各自索引,保证每位教师的头像颜色不同且固定。


5. UI 组件详解

5.1 TeacherCard:教师卡片组件

TeacherCard 是一个自定义 @Component,接收三个参数:

typescript 复制代码
struct TeacherCard {
  private teacher: Teacher = TEACHERS_DATA[0];
  private onSit?: () => void;    // 入座回调
  private onLeave?: () => void;  // 离开回调
5.1.1 头像设计
typescript 复制代码
Text(this.teacher.name.substring(0, 1))
  .width(48).height(48)
  .fontSize(20).fontWeight(FontWeight.Bold)
  .fontColor(TEXT_WHITE)
  .textAlign(TextAlign.Center)
  .backgroundColor(getAvatarColor(this.teacher.colorIndex))
  .borderRadius(24)   // 圆形

substring(0, 1) 取姓氏首字作为头像内容,48x48 圆角矩形(borderRadius: 24 即完全圆形)。颜色来自 AVATAR_COLORS 数组,通过 colorIndex 索引。

5.1.2 状态标签
typescript 复制代码
Text(this.teacher.status === SeatStatus.SEATED ? '🟢 在座' : '🔴 离座')
  .fontColor(this.teacher.status === SeatStatus.SEATED ? ACCENT_GREEN : ACCENT_RED)

使用 Emoji 作为状态指示器,简洁直观。

5.1.3 累计时长显示
typescript 复制代码
if (this.teacher.totalMinutes > 0) {
  Text(`今日在座 ${formatDuration(this.teacher.totalMinutes)}`)
}

只有累计时长 > 0 时才显示,避免界面冗余。

5.1.4 操作按钮的状态控制
typescript 复制代码
Button('入座')
  .enabled(this.teacher.status !== SeatStatus.SEATED)

Button('离开')
  .enabled(this.teacher.status !== SeatStatus.AWAY)

enabled 属性控制按钮是否可点击。当教师已在座时,"入座"按钮置灰不可点击;反之亦然。这种互斥状态设计避免了无效操作。

5.2 RecordItem:记录条目组件

typescript 复制代码
struct RecordItem {
  private record: SeatRecord = INITIAL_RECORDS[0];
  private isLatest: boolean = false;

使用 Row 水平布局显示三部分信息:

复制代码
[类型图标]  [教师姓名 + 操作类型]  [时间]
  ⬇️ / ⬆️      张老师 · 入座          09:30:25

最新一条记录背景高亮(isLatest === true 时使用 CARD_BG2 背景色),方便用户快速定位最近的操作。

5.3 主页面结构

主页面 TeacherSeatRecord 的结构层次:

复制代码
Column (全屏深色背景)
  ├── Row (顶部标题栏:返回 + 标题 + 重置)
  ├── Row (统计卡片三列)
  │     ├── Column (在座人数)
  │     ├── Column (教师总数)
  │     └── Column (离座人数)
  ├── Row (Tab 切换:教师列表 / 出入记录)
  └── Scroll (内容区域)
        ├── Grid (教师列表,2列网格)
        │     ├── GridItem → TeacherCard × 8
        │     └── ...
        └── Scroll (记录列表)
              ├── RecordItem × N
              └── ...

6. 业务逻辑与状态管理

6.1 @State 状态管理

ArkTS 中,@State 装饰器标记的变量发生变化时,UI 自动重新渲染:

typescript 复制代码
@State private teachers: Teacher[] = JSON.parse(JSON.stringify(TEACHERS_DATA));
@State private records: SeatRecord[] = JSON.parse(JSON.stringify(INITIAL_RECORDS));
@State private currentTab: number = 0;

深拷贝陷阱 :为什么用 JSON.parse(JSON.stringify(...))

因为 ArkTS 中 const 声明的数组或对象如果直接赋值给 @State,多个 @State 会共享引用。使用深拷贝确保每个 @State 拥有独立的数据副本,避免意外修改模拟数据源。

6.2 计算属性:在座人数

typescript 复制代码
get seatedCount(): number {
  return this.teachers.filter(t => t.status === SeatStatus.SEATED).length;
}

get 访问器在 ArkTS 中相当于计算属性,每次访问时重新计算。虽然 @State teachers 变化时 UI 会重新渲染,但 seatedCount 的计算开销很小(只遍历 8 个元素),无需做性能优化。

6.3 handleSit:入座逻辑

typescript 复制代码
handleSit(teacherId: number): void {
  const idx = this.teachers.findIndex(t => t.id === teacherId);
  if (idx === -1 || this.teachers[idx].status === SeatStatus.SEATED) return;

  const now = getCurrentTime();
  const today = getCurrentDate();

  this.teachers[idx].status = SeatStatus.SEATED;
  this.teachers[idx].seatedTime = now;

  // 添加记录(最新在最前面)
  this.records = [
    {
      id: this.nextRecordId++,
      teacherId: teacherId,
      teacherName: this.teachers[idx].name,
      type: RecordType.SIT,
      time: now,
      date: today
    },
    ...this.records
  ];
}

逻辑要点:

  1. 防御检查findIndex === -1 或已在座时直接返回
  2. 获取当前时间 :使用 getCurrentTime()getCurrentDate() 两个工具函数
  3. 更新状态 :修改 statusseatedTime
  4. 追加记录 :使用展开运算符 ...this.records 将新记录插入数组头部(最新在最前)

6.4 handleLeave:离开逻辑(核心算法)

typescript 复制代码
handleLeave(teacherId: number): void {
  const idx = this.teachers.findIndex(t => t.id === teacherId);
  if (idx === -1 || this.teachers[idx].status === SeatStatus.AWAY) return;

  const now = getCurrentTime();
  const today = getCurrentDate();

  // 计算本次在座时长
  const seatedTime = this.teachers[idx].seatedTime;
  if (seatedTime) {
    const sh = Number(seatedTime.substring(0, 2)); // 入座小时
    const sm = Number(seatedTime.substring(3, 5)); // 入座分钟
    const eh = Number(now.substring(0, 2));         // 当前小时
    const em = Number(now.substring(3, 5));         // 当前分钟
    const diffMinutes = (eh * 60 + em) - (sh * 60 + sm);
    if (diffMinutes > 0) {
      this.teachers[idx].totalMinutes += diffMinutes;
    }
  }
  // ...
}

时长计算算法

  1. seatedTime 字符串 "09:30:25" 中提取小时和分钟
  2. now 字符串 "14:45:12" 中提取当前小时和分钟
  3. 将两者分别转为分钟数:hours * 60 + minutes
  4. 相减得到在座分钟数

这种算法比 Date.parse() 或时间戳减法更简洁、更可控,因为我们的时间格式是固定的 HH:mm:ss

为什么不用 Date 对象相减? 因为 Dayjs / date-fns 等库在 ArkTS 中不可用,原生 Date 的计算涉及时区和跨天问题,对于"同一天内的时长计算"这个简单场景,字符串解析更可靠。

6.5 handleReset:重置逻辑

typescript 复制代码
handleReset(): void {
  AlertDialog.show({
    title: '确认重置',
    message: '将清除所有教师的在座状态和今日时长,确定吗?',
    primaryButton: {
      value: '取消',
      action: () => {}
    },
    secondaryButton: {
      value: '确定重置',
      fontColor: ACCENT_RED,
      action: () => {
        this.teachers = JSON.parse(JSON.stringify(TEACHERS_DATA));
        this.records = JSON.parse(JSON.stringify(INITIAL_RECORDS));
        this.nextRecordId = INITIAL_RECORDS.length + 1;
      }
    }
  });
}

重置操作有破坏性 ,加入 AlertDialog 确认弹窗来防止误操作。确定后重新深拷贝初始数据,恢复初始状态。


7. ArkTS 语法避坑指南

在开发过程中我们遇到了几个 ArkTS 语法的"坑",这里记录下解决方案。

7.1 非空断言 ! 不支持

错误写法

typescript 复制代码
const [sh, sm] = this.teachers[idx].seatedTime!.split(':').map(Number);

ArkTS 不允许 使用 ! 非空断言操作符。

解决方案:先用局部变量 + 类型收窄:

typescript 复制代码
const seatedTime = this.teachers[idx].seatedTime;
if (seatedTime) {
  // 这里 ArkTS 编译器自动推断 seatedTime 为 string 类型
  const sh = Number(seatedTime.substring(0, 2));
}

7.2 .map(Number) 不支持

错误写法

typescript 复制代码
result.split(':').map(Number)

ArkTS 不允许将构造函数/类型作为回调传递给 .map()

解决方案:使用箭头函数封装:

typescript 复制代码
result.split(':').map(item => Number(item))

或者更彻底地,直接用 substring 避开 split 和 map:

typescript 复制代码
const sh = Number(seatedTime.substring(0, 2));
const sm = Number(seatedTime.substring(3, 5));

7.3 数组解构需谨慎

虽然 ArkTS 支持 const [a, b] = arr 语法,但在某些场景下(尤其是和 .map() 链式调用结合时)可能触发编译问题。

推荐做法:对于简单场景,逐个声明变量更安全。

7.4 @State 引用共享问题

typescript 复制代码
// 错误:teachers 和 INITIAL_TEACHERS 共享同一份引用
@State private teachers: Teacher[] = TEACHERS_DATA;

// 正确:深拷贝一份独立数据
@State private teachers: Teacher[] = JSON.parse(JSON.stringify(TEACHERS_DATA));

如果不深拷贝,重置操作 this.teachers = TEACHERS_DATA 实际上是同一份数据,而且对 teachers 的修改会污染 TEACHERS_DATA 常量。

7.5 回调函数类型定义

在组件中传递函数回调时,需要明确声明类型:

typescript 复制代码
private onSit?: () => void;    // 无参数无返回值回调
private onLeave?: () => void;

调用时使用可选链:

typescript 复制代码
this.onSit?.();
this.onLeave?.();

7.6 颜色常量字符串需显式指定类型

ArkTS 中颜色字符串必须显式标注 string 类型:

typescript 复制代码
const BG_DARK = '#0A0A1A';   // 自动推断为 string,没问题

但如果需要将颜色变量传递给 .backgroundColor(),要确保类型明确。

7.7 forEach 回调参数类型

ForEach 的回调参数需要标注类型:

typescript 复制代码
ForEach(this.teachers, (teacher: Teacher) => { ... })
ForEach(this.records, (record: SeatRecord, index: number) => { ... })

不标注类型在某些场景下可能导致编译错误。


8. UI 布局细节分析

8.1 深色主题配色方案

复制代码
背景色:     #0A0A1A (深空蓝黑)
卡片背景:   #1A1A2E (藏青)
卡片背景2:  #16213E (深蓝)
主文字:     #FFFFFF (白色)
次要文字:   #AAAAAA (灰色)
弱化文字:   #666666 (暗灰)
强调蓝:     #4D96FF
强调绿:     #6BCB77
强调橙:     #FFA94D
强调红:     #FF6B6B
强调紫:     #9B59B6
强调青:     #00D2FF

这个配色方案灵感来自 VS Code 的深色主题和 Tailwind CSS 的调色板,层次分明,视觉舒适。

8.2 顶部标题栏

typescript 复制代码
Row() {
  Button() { Text('←') }     // 返回
  Blank().layoutWeight(1)    // 弹性占位
  Text('教师座椅出入记录')     // 标题
  Blank().layoutWeight(1)    // 弹性占位
  Button() { Text('重置') }   // 重置
}

Blank().layoutWeight(1) 是 ArkTS 中实现弹性空白 的标准方式,相当于 Flexbox 中的 flex: 1

8.3 统计卡片三列布局

typescript 复制代码
Row() {
  Column() { /* 在座人数 */ }
    .layoutWeight(1)
    .margin({ right: 6 })
  Column() { /* 教师总数 */ }
    .layoutWeight(1)
    .margin({ left: 6, right: 6 })
  Column() { /* 离座人数 */ }
    .layoutWeight(1)
    .margin({ left: 6 })
}

三列等宽分布,中间列两侧都有 margin,边列只有单侧 margin,保证间距均匀。

每个统计卡片的数字字号 32、加粗,直观醒目。

8.4 Tab 切换按钮

typescript 复制代码
Button('👨‍🏫 教师列表')
  .layoutWeight(1)
  .backgroundColor(this.currentTab === 0 ? ACCENT_BLUE : CARD_BG)

Button('📋 出入记录')
  .layoutWeight(1)
  .backgroundColor(this.currentTab === 1 ? ACCENT_BLUE : CARD_BG)

当前选中的 Tab 使用 ACCENT_BLUE(蓝),未选中的使用 CARD_BG(暗)。通过 currentTab 状态变量控制高亮。

8.5 网格布局展示教师列表

使用 Grid 组件的 columnsTemplate 实现 2 列网格:

typescript 复制代码
Grid() {
  ForEach(this.teachers, (teacher: Teacher) => {
    GridItem() {
      TeacherCard({ teacher, onSit, onLeave })
    }
  })
}
.columnsTemplate('1fr 1fr')  // 两列等宽
.columnsGap(8)               // 列间距 8
.rowsGap(8)                  // 行间距 8

1fr 1fr 表示两列各占一半宽度,类似 CSS Grid 的 1fr 1fr。对于 8 位教师,会渲染为 4 行 2 列。

8.6 记录列表与空状态

typescript 复制代码
if (this.records.length === 0) {
  Column() {
    Text('📭').fontSize(48)
    Text('暂无出入记录').fontSize(16)
  }
  .height(200)
  .justifyContent(FlexAlign.Center)
} else {
  ForEach(this.records, (record, index) => {
    RecordItem({ record, isLatest: index === 0 })
  })
}

列表为空时显示友好的空状态提示。Scroll 包裹确保记录多时可以滚动。

8.7 Scroll + layoutWeight 实现自适应高度

typescript 复制代码
Scroll() {
  Column() {
    Grid() { ... }
  }
}
.layoutWeight(1)

layoutWeight(1) 让 Scroll 占满父容器剩余空间,这是 ArkTS 中实现"撑满剩余高度"的标准做法。


9. 完整代码解析

9.1 工具函数

typescript 复制代码
/** 获取当前时间字符串 HH:mm:ss */
function getCurrentTime(): string {
  const now = new Date();
  const h = now.getHours().toString().padStart(2, '0');
  const m = now.getMinutes().toString().padStart(2, '0');
  const s = now.getSeconds().toString().padStart(2, '0');
  return `${h}:${m}:${s}`;
}

/** 获取当前日期字符串 YYYY-MM-DD */
function getCurrentDate(): string {
  const now = new Date();
  const y = now.getFullYear();
  const m = (now.getMonth() + 1).toString().padStart(2, '0');
  const d = now.getDate().toString().padStart(2, '0');
  return `${y}-${m}-${d}`;
}

/** 格式化时长(分钟 → X小时X分钟) */
function formatDuration(minutes: number): string {
  if (minutes < 60) {
    return `${minutes}分钟`;
  }
  const h = Math.floor(minutes / 60);
  const m = minutes % 60;
  return m > 0 ? `${h}小时${m}分钟` : `${h}小时`;
}

三个工具函数都很简单,但必不可少:

  • getCurrentTime:返回 HH:mm:ss 格式,用于记录操作时间戳和计算时长
  • getCurrentDate:返回 YYYY-MM-DD 格式,用于记录日期
  • formatDuration:将纯分钟数转为人类可读的"X小时X分钟"格式

padStart(2, '0') 确保时间数字始终是两位数。

9.2 常量与类型定义(文件头)

typescript 复制代码
import { router } from '@kit.ArkUI';

// 颜色常量
const BG_DARK = '#0A0A1A';
const CARD_BG = '#1A1A2E';
const CARD_BG2 = '#16213E';
const TEXT_WHITE = '#FFFFFF';
const TEXT_GRAY = '#AAAAAA';
const TEXT_DIM = '#666666';
const ACCENT_BLUE = '#4D96FF';
const ACCENT_GREEN = '#6BCB77';
const ACCENT_RED = '#FF6B6B';

// 枚举
enum SeatStatus { AWAY = 'away', SEATED = 'seated' }
enum RecordType { SIT = 'sit', LEAVE = 'leave' }

// 接口
interface Teacher { /* ... */ }
interface SeatRecord { /* ... */ }

所有常量、类型定义集中在文件顶部,便于维护。颜色常量使用 const 声明,编译期即确定值。

9.3 完整主页面代码结构

复制代码
@Entry
@Component
struct TeacherSeatRecord {
  // 状态变量
  @State teachers: Teacher[]
  @State records: SeatRecord[]
  @State currentTab: number

  // 计算属性
  get seatedCount(): number

  // 业务方法
  handleSit(teacherId: number): void
  handleLeave(teacherId: number): void
  handleReset(): void

  // UI 构建
  build() { /* ... */ }
}

@Entry 装饰器标记该组件是一个页面入口,可以被路由导航。

9.4 组件间通信

父 → 子(属性传递)

typescript 复制代码
TeacherCard({
  teacher: teacher,
  onSit: () => this.handleSit(teacher.id),
  onLeave: () => this.handleLeave(teacher.id)
})

子 → 父(回调函数)

typescript 复制代码
// 子组件(TeacherCard)内部
Button('入座').onClick(() => { this.onSit?.(); })

这是 ArkTS 中最推荐的组件通信模式:数据向下传递,事件向上传递。


10. 运行效果与演示

10.1 界面预览

APP 启动后,首页为深色背景,展示 8 位教师的卡片网格视图,每位教师显示:

  • 姓氏首字头像(带彩色背景)
  • 姓名(如「张老师」)
  • 状态(🟢 在座 / 🔴 离座)
  • 今日在座时长(如有)
  • 两个操作按钮:入座(绿色)和离开(红色)

顶部展示三个统计卡片:在座人数、教师总数、离座人数。

顶部右侧有「重置」按钮,点击后弹出确认弹窗。

10.2 操作流程演示

场景 1:张老师入座

  1. 用户在 8 位教师中找到「张老师」
  2. 此时张老师状态为 🔴 离座,「入座」按钮可点击
  3. 点击「入座」
  4. 张老师状态变为 🟢 在座,「入座」按钮置灰,「离开」按钮可用
  5. 在「出入记录」Tab 中看到新记录:「张老师 · 入座 · 09:30:25」

场景 2:张老师离开

  1. 点击「离开」按钮
  2. 系统自动计算在座时长(假设入座 09:30,离开 11:45,则时长 = 135 分钟 = 2小时15分钟)
  3. 张老师状态恢复为 🔴 离座
  4. 今日在座时长显示「今日在座 2小时15分钟」
  5. 记录列表追加「张老师 · 离开 · 11:45:12」

场景 3:查看历史记录

  1. 切换到「📋 出入记录」Tab
  2. 按时间倒序显示所有操作记录,最新一条高亮背景

场景 4:重置数据

  1. 点击顶部「重置」按钮
  2. 弹出「确认重置」对话框
  3. 确认后,所有状态恢复到初始值,记录清空

10.3 核心数据流

复制代码
[用户操作]                        [状态更新]                    [UI 自动刷新]
  点击入座  →  handleSit()   →  @State teachers/records  →  UI re-render
  点击离开  →  handleLeave()  →  @State teachers/records  →  UI re-render
  点击重置  →  handleReset()  →  @State teachers/records  →  UI re-render
  切换Tab   →  currentTab=1   →  @State currentTab       →  显示记录列表

11. 扩展与优化方向

11.1 功能扩展

扩展方向 实现思路 复杂度
本地持久化 使用 @ohos.data.preferencesrelationalStore 保存数据 中等
导出记录 生成 CSV 文件并分享
搜索/筛选 添加搜索框,按姓名筛选教师
统计分析 显示日/周/月的在座时长趋势图表 中高
多日数据 按日期分组显示历史记录 中等
自定义教师 添加/删除教师的功能 中等
主题切换 浅色/深色主题切换

11.2 持久化方案

当前数据存储在内存中,APP 退出后丢失。使用 Preferences 存储的示例:

typescript 复制代码
import { preferences } from '@kit.ArkData';

async function saveData(teachers: Teacher[]) {
  const prefs = await preferences.getPreferences(getContext(), 'seat_record');
  await prefs.put('teachers', JSON.stringify(teachers));
  await prefs.flush();
}

11.3 性能优化

对于当前 8 位教师、数十条记录的场景,无需性能优化。但如果扩展到 100+ 教师,可以考虑:

  • 虚拟列表 :使用 LazyForEach 替代 ForEach,只渲染可见区域的条目
  • 计算属性缓存:对频繁访问的计算结果做缓存
  • 状态细分:避免大数据对象整体更新

11.4 多设备适配

HarmonyOS 的优势之一是多设备适配。可以:

  • 手机端使用 Grid 2 列布局
  • 平板端使用 Grid 3-4 列布局
  • 手表端简化为单列列表

通过 breakpointSystem 监听屏幕宽度变化,动态调整 columnsTemplate

typescript 复制代码
.columnsTemplate(this.isWideScreen ? '1fr 1fr 1fr' : '1fr 1fr')

12. 总结与心得

12.1 项目成果

我们用了不到 550 行 ArkTS 代码(含注释)构建了一个完整的教师座椅出入记录 APP,实现了:

  • 8 位教师的实时状态管理
  • 入座/离开操作与时长自动计算
  • 累计在座时长统计
  • 历史出入记录查询
  • 在座/离座人数统计
  • 重置与确认弹窗

12.2 ArkTS 开发体会

优势

  1. 声明式 UI 生产力高:用代码描述 UI,无需 XML 布局,开发效率高
  2. TypeScript 基础:前端开发者上手快,类型系统在大型项目中减少大量 bug
  3. 响应式状态@State + 自动重新渲染,减少样板代码
  4. 编译性能:Ark Compiler 的 AOT 编译使启动速度快
  5. 原生组件丰富:Grid、Scroll、Button、Text 等原生组件性能好

不足

  1. 生态较小:第三方组件库少,很多功能需要自己实现
  2. 语法限制较多!.map(Number)、部分解构语法等不支持,需要适应
  3. 社区资源少:遇到问题时中文资料有限
  4. 调试工具待完善:Inspector 和 Profiler 功能不如 Chrome DevTools 成熟

12.3 经验教训

  1. 数据不变性:始终使用深拷贝创建新数组/对象,不要直接修改原始数据
  2. 类型标注习惯ForEach 回调、函数参数等处主动标注类型,减少编译错误
  3. 远离 TS 高级语法:ArkTS 是 TypeScript 的子集,装饰器、非空断言等高级特性不支持
  4. 组件拆分适度:TeacherCard 和 RecordItem 两个子组件拆得恰到好处,不多不少
  5. 注释写中文:代码注释用中文,方便团队沟通

12.4 未来展望

随着 HarmonyOS 生态的持续发展,ArkTS 的语法限制会逐步放开,组件库会越来越丰富。这个教师座椅出入记录 APP 作为一个中等复杂度的 Demo,覆盖了 ArkTS 开发的常见场景:状态管理、组件通信、列表渲染、表单交互、时间计算等。

期待鸿蒙原生应用生态越来越好!


附录

A. 完整代码

完整源代码可在 entry/src/main/ets/pages/TeacherSeatRecord.ets 中查看。

B. 使用的 ArkTS API 参考

API 用途
@Entry 页面入口标识
@Component 组件声明
@State 响应式状态
build() UI 构建函数
Column / Row 线性布局
Grid / GridItem 网格布局
Scroll 可滚动容器
Button 按钮
Text 文本显示
Blank 弹性空白
ForEach 列表渲染
AlertDialog.show() 弹窗
router.pushUrl/back 页面导航

C. 涉及的 HarmonyOS 开发概念

  • ArkTS 声明式 UI 编程范式
  • 组件化开发模式
  • 单向数据流
  • 状态与 UI 的自动同步
  • 页面路由与导航

本文由 AtomCode 基于 HarmonyOS API 24 ArkTS 开发实战编写

相关推荐
踏着七彩祥云的小丑1 小时前
嵌入式测试第 32 天:升级测试:固件OTA升级、断点续传、回滚测试
单片机·嵌入式硬件·学习
小陈phd1 小时前
Text2SQL智能体学习笔记(二)——NL2SQL落地的隐形基石:元数据库
数据库·笔记·学习
狼哥16861 小时前
蛋糕美食元服务_订单实现指南
ui·harmonyos
Swift社区1 小时前
鸿蒙游戏如何实现多端一致性?
游戏·华为·harmonyos
木咺吟1 小时前
【鸿蒙原生应用开发实战】第一篇:项目初始化与架构设计——从零搭建“阅迹“阅读应用
华为·harmonyos
组合缺一1 小时前
SolonCode(编码智能体)支持鸿蒙 PC
java·华为·ai·ai编程·harmonyos·solon·soloncode
xym2 小时前
鸿蒙 Node-API 自用整理
harmonyos
踏着七彩祥云的小丑2 小时前
Go学习第4天:条件、循环语句+函数
学习·golang·go
yuegu7772 小时前
HarmonyOS应用<节气通>开发第24篇:响应式布局设计
深度学习·harmonyos