在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

API Version: HarmonyOS API 24 (HarmonyOS 4.0+)
语言: ArkTS(鸿蒙原生声明式 UI 框架)
开发工具: DevEco Studio
完整源码: 见文章底部


目录

  1. 项目背景与需求分析
  2. 技术选型与架构设计
  3. 项目搭建与配置
  4. 数据模型设计
  5. UI 组件详解
  6. 业务逻辑与状态管理
  7. ArkTS 语法避坑指南
  8. 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 配置文件注册所有页面:

{
  "src": [
    "pages/Index",
    "pages/TeacherSeatRecord"
  ]
}

3.3 页面导航

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

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 枚举:教师状态

enum SeatStatus {
  AWAY = 'away',    // 离座
  SEATED = 'seated' // 在座
}

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

4.2 枚举:操作类型

enum RecordType {
  SIT = 'sit',      // 入座
  LEAVE = 'leave'   // 离开
}

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

4.3 接口:教师数据

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

关键设计点:

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

4.4 接口:出入记录

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 常量与模拟数据

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,接收三个参数:

struct TeacherCard {
  private teacher: Teacher = TEACHERS_DATA[0];
  private onSit?: () => void;    // 入座回调
  private onLeave?: () => void;  // 离开回调
5.1.1 头像设计
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 状态标签
Text(this.teacher.status === SeatStatus.SEATED ? '🟢 在座' : '🔴 离座')
  .fontColor(this.teacher.status === SeatStatus.SEATED ? ACCENT_GREEN : ACCENT_RED)

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

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

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

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

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

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

5.2 RecordItem:记录条目组件

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 自动重新渲染:

@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 计算属性:在座人数

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

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

6.3 handleSit:入座逻辑

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:离开逻辑(核心算法)

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:重置逻辑

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 非空断言 ! 不支持

错误写法

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

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

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

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

7.2 .map(Number) 不支持

错误写法

result.split(':').map(Number)

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

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

result.split(':').map(item => Number(item))

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

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 引用共享问题

// 错误: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 回调函数类型定义

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

private onSit?: () => void;    // 无参数无返回值回调
private onLeave?: () => void;

调用时使用可选链:

this.onSit?.();
this.onLeave?.();

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

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

const BG_DARK = '#0A0A1A';   // 自动推断为 string,没问题

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

7.7 forEach 回调参数类型

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

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 顶部标题栏

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

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

8.3 统计卡片三列布局

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 切换按钮

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 列网格:

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 记录列表与空状态

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 实现自适应高度

Scroll() {
  Column() {
    Grid() { ... }
  }
}
.layoutWeight(1)

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


9. 完整代码解析

9.1 工具函数

/** 获取当前时间字符串 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 常量与类型定义(文件头)

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 组件间通信

父 → 子(属性传递)

TeacherCard({
  teacher: teacher,
  onSit: () => this.handleSit(teacher.id),
  onLeave: () => this.handleLeave(teacher.id)
})

子 → 父(回调函数)

// 子组件(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 存储的示例:

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

.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 开发实战编写

Logo

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

更多推荐