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



API Version: HarmonyOS API 24 (HarmonyOS 4.0+)
语言: ArkTS(鸿蒙原生声明式 UI 框架)
开发工具: DevEco Studio
完整源码: 见文章底部
目录
- 项目背景与需求分析
- 技术选型与架构设计
- 项目搭建与配置
- 数据模型设计
- UI 组件详解
- 业务逻辑与状态管理
- ArkTS 语法避坑指南
- UI 布局细节分析
- 完整代码解析
- 运行效果与演示
- 扩展与优化方向
- 总结与心得
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
];
}
逻辑要点:
- 防御检查:
findIndex === -1或已在座时直接返回 - 获取当前时间:使用
getCurrentTime()和getCurrentDate()两个工具函数 - 更新状态:修改
status和seatedTime - 追加记录:使用展开运算符
...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;
}
}
// ...
}
时长计算算法:
- 从
seatedTime字符串"09:30:25"中提取小时和分钟 - 从
now字符串"14:45:12"中提取当前小时和分钟 - 将两者分别转为分钟数:
hours * 60 + minutes - 相减得到在座分钟数
这种算法比 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:张老师入座
- 用户在 8 位教师中找到「张老师」
- 此时张老师状态为 🔴 离座,「入座」按钮可点击
- 点击「入座」
- 张老师状态变为 🟢 在座,「入座」按钮置灰,「离开」按钮可用
- 在「出入记录」Tab 中看到新记录:「张老师 · 入座 · 09:30:25」
场景 2:张老师离开
- 点击「离开」按钮
- 系统自动计算在座时长(假设入座 09:30,离开 11:45,则时长 = 135 分钟 = 2小时15分钟)
- 张老师状态恢复为 🔴 离座
- 今日在座时长显示「今日在座 2小时15分钟」
- 记录列表追加「张老师 · 离开 · 11:45:12」
场景 3:查看历史记录
- 切换到「📋 出入记录」Tab
- 按时间倒序显示所有操作记录,最新一条高亮背景
场景 4:重置数据
- 点击顶部「重置」按钮
- 弹出「确认重置」对话框
- 确认后,所有状态恢复到初始值,记录清空
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.preferences 或 relationalStore 保存数据 |
中等 |
| 导出记录 | 生成 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 开发体会
优势:
- 声明式 UI 生产力高:用代码描述 UI,无需 XML 布局,开发效率高
- TypeScript 基础:前端开发者上手快,类型系统在大型项目中减少大量 bug
- 响应式状态:
@State+ 自动重新渲染,减少样板代码 - 编译性能:Ark Compiler 的 AOT 编译使启动速度快
- 原生组件丰富:Grid、Scroll、Button、Text 等原生组件性能好
不足:
- 生态较小:第三方组件库少,很多功能需要自己实现
- 语法限制较多:
!、.map(Number)、部分解构语法等不支持,需要适应 - 社区资源少:遇到问题时中文资料有限
- 调试工具待完善:Inspector 和 Profiler 功能不如 Chrome DevTools 成熟
12.3 经验教训
- 数据不变性:始终使用深拷贝创建新数组/对象,不要直接修改原始数据
- 类型标注习惯:
ForEach回调、函数参数等处主动标注类型,减少编译错误 - 远离 TS 高级语法:ArkTS 是 TypeScript 的子集,装饰器、非空断言等高级特性不支持
- 组件拆分适度:TeacherCard 和 RecordItem 两个子组件拆得恰到好处,不多不少
- 注释写中文:代码注释用中文,方便团队沟通
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 开发实战编写
更多推荐

所有评论(0)