欢迎光临
我们一直在努力

鸿蒙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) 取姓氏首字作为头像内容,48×48 圆角矩形(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)
    未经允许不得转载:171主机测评 » 鸿蒙HarmonyOS ArkTS 实战:教师座椅出入记录 APP 从零到一
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址