基于HarmonyOS API 24与ArkTS声明式范式的人事考勤打卡排班系统实战:从HarmonyOS 6.1.1新特性到声明式UI状态管理与多模态弹窗交互的全栈架构深度剖析
一、技术背景:鸿蒙生态与声明式UI范式的演进
1.1 HarmonyOS 6.1.1 与 ArkTS 语言体系概述
HarmonyOS 作为华为推出的面向全场景的分布式操作系统,其核心愿景是实现"一次开发,多端部署"的跨设备协同能力。在 HarmonyOS 6.1.1 版本中,系统进一步强化了 ArkUI 声明式UI框架的渲染性能与组件表达能力,同时优化了 ArkTS 语言的类型推断机制和编译期检查能力。ArkTS 是在 TypeScript 基础上扩展而来的应用开发语言,它保留了 TypeScript 的静态类型系统优势,同时引入了鸿蒙特有的装饰器语法和状态管理模型。
ArkTS 语言的核心设计理念是"声明式编程"——开发者只需描述界面"应该是什么样子",而无需关心界面"如何一步步构建"。这种范式转变使得 UI 代码更加简洁、可预测性更强,且与数据状态自动同步。在 HarmonyOS ArkTS API 24 中,这一理念被贯彻到了每一个组件和装饰器的设计中,从 @Entry、@Component 到 @State、@Builder,形成了一套完整的声明式UI开发体系。
1.2 声明式UI范式与 ArkUI 框架架构
ArkUI 框架是 HarmonyOS 应用 UI 层的核心引擎,它采用了三层架构设计:UI 语义层负责解析 ArkTS 声明式语法并构建组件树;UI 渲染层负责将组件树映射为可渲染的布局节点树;UI 事件层负责处理用户交互事件并将其反馈到状态管理系统中。这三层协同工作,实现了从"状态变化"到"UI 更新"的自动化数据驱动渲染流水线。
在声明式UI范式中,状态(State)是驱动 UI 渲染的唯一数据源。开发者通过 @State、@Prop、@Link、@Provide/@Consume 等装饰器声明组件的状态变量,当这些变量的值发生变化时,ArkUI 框架会自动触发与之关联的 UI 组件进行重新渲染。这种"数据驱动视图"的模式彻底告别了传统命令式编程中手动操作 DOM 节点的繁琐方式,极大提升了开发效率和代码可维护性。
1.3 ArkTS 语言特性与类型系统
ArkTS 继承了 TypeScript 的强类型特性,同时针对鸿蒙 UI 开发场景做了深度定制。在 ArkTS 中,interface 关键字用于定义数据结构契约,所有状态变量和组件参数都必须具有明确的类型标注。这种设计在编译期就能捕获大部分类型错误,避免了运行时因类型不匹配导致的崩溃风险。
此外,ArkTS 引入了 enum 枚举类型用于管理有限的离散值集合(如 Tab 切换状态),以及 @Builder 装饰器用于定义可复用的 UI 片段构建函数。@Builder 的本质是一种语法糖,它允许开发者将复杂的 UI 结构封装为独立函数,在 build() 方法中通过函数调用方式进行引用,从而实现 UI 的模块化拆分与复用,而无需创建独立的 @Component 组件。
1.4 HarmonyOS 6.1.1 新特性与 API 24 能力
HarmonyOS 6.1.1 基于 HarmonyOS API 24 构建,在此版本中,ArkUI 框架带来了多项重要增强。首先是 Progress 组件的动画能力增强,支持通过 animation 修饰器绑定曲线动画,使进度环的填充过程更加平滑自然。其次是 linearGradient 线性渐变属性的全面支持,开发者可以为任意容器组件设置双色或多色渐变背景,配合 angle 参数控制渐变角度。
同时,API 24 对 Flex 布局组件进行了优化,支持 FlexWrap.Wrap 自动换行模式,这在日历网格等需要动态排列子元素的场景中尤为实用。Stack 堆叠布局组件的 zIndex 属性也得到了增强,使得弹窗遮罩层与内容层的层级控制更加精确可靠。此外,Scroll 组件的 scrollBar 属性支持 BarState.Off 隐藏滚动条,配合 constraintSize 的最大高度约束,实现了内容区域的自适应滚动体验。
1.5 业务背景:人事考勤打卡排班系统的需求场景
在现代企业管理中,考勤管理是人力资源部门最基础也是最核心的工作之一。传统考勤方式依赖物理打卡机或指纹设备,存在部署成本高、数据滞后、无法移动化操作等痛点。随着移动互联网和企业数字化转型的深入,基于移动端的考勤管理系统逐渐成为企业标配。
本文分析的正是这样一款面向企业员工的移动端考勤管理应用——“准时宝”。该应用集成了打卡、统计、排班、请假和我的五大核心模块,覆盖了员工日常考勤的全生命周期。从上班打卡时的GPS定位校验,到月度出勤率的可视化统计;从本月排班日历的网格化展示,到换班申请的同事选择交互;从请假申请的底部抽屉表单,到加班登记的步进器补偿方式选择——每一个功能模块都体现了移动端企业应用的典型交互模式和技术实现方案。
接下来,我们将从架构设计到逐行代码,对这套基于 HarmonyOS API 24 构建的考勤系统进行全方位深度剖析。
二、系统架构总览与分层设计
2.1 整体架构层次图
在深入代码细节之前,我们首先从宏观层面理解整个应用的架构分层。该应用采用了"数据层 — 状态层 — UI层 — 交互层"的四层架构模型,每一层都有明确的职责边界和依赖关系。
从架构图可以看出,数据定义层位于最底层,它定义了应用中所有数据的结构契约和静态初始值。状态管理层依赖于数据定义层,通过 @State 装饰器将数据绑定到具体的 UI 组件上。UI 渲染层由五个内容组件和一个主入口组件构成,每个组件内部维护自己的状态变量。交互层是用户与系统之间的桥梁,通过 onClick、onChange 等事件回调驱动状态变化,进而触发 UI 重新渲染。
2.2 页面导航流程图
应用采用底部 Tab 切换的导航模式,这是移动端应用最常见的导航范式之一。通过 enum AttendTab 定义五个 Tab 的标识符,配合 ATTEND_TABS 元数据数组提供每个 Tab 的图标和标签文本。主入口组件通过 @State currentTab 状态变量追踪当前激活的 Tab,在 build() 方法中使用 if/else 条件分支渲染对应的内容组件。
这种导航模式的核心优势在于:用户可以快速在五个功能模块间切换,且每个模块的 UI 状态是独立维护的,切换 Tab 不会丢失当前模块的内部状态(如已展开的弹窗、已选择的日期等)。同时,底部 Tab 的视觉反馈通过 fontColor 和 backgroundColor 的条件判断实现高亮,让用户清晰地知道当前所处的功能页面。
三、类型定义层深度解析
3.1 ClockLog 打卡记录接口
interface ClockLog {
id: number
date: string
on: string
off: string
state: string
hours: string
}

这是打卡记录的数据结构定义。在 ArkTS 中,interface 关键字用于声明一个接口类型,它定义了对象的属性名和属性类型的契约。与 TypeScript 不同的是,ArkTS 中的 interface 更加严格——所有属性必须显式标注类型,不允许使用可选属性(?)在组件状态上下文中,且不支持方法声明,仅用于纯数据结构描述。
id 字段类型为 number,它是每条记录的唯一标识符。在 ForEach 循环渲染列表时,id 被用作 keyGenerator(键值生成器),确保列表项的高效更新与虚拟DOM diff 优化。date 字段存储日期描述字符串,如 '08-26 今天',这里将日期和语义化标记拼接在一起,是移动端列表展示的常见做法。
on 和 off 分别表示上班打卡时间和下班打卡时间。它们的值可能是具体的时间字符串(如 '08:52'),也可能是特殊标记(如 '休息' 或 '--')。这种宽松的字符串约定虽然牺牲了一些类型安全性,但在展示层处理上更加灵活。state 字段记录考勤状态('正常'、'加班'、'迟到'、'休息日'、'值班'、'已上班'),它将直接驱动 UI 层的颜色映射逻辑。hours 字段存储工时统计,格式如 '8.5h' 或 '-'。
3.2 MonthRate 月度出勤率接口
interface MonthRate {
id: number
month: string
rate: number
}

月度出勤率接口定义了三个字段。month 是月份标签字符串(如 '3月'、'4月'),用于柱状图的横轴展示。rate 是出勤率数值,类型为 number,取值范围为 0-100。注意这里使用 number 而非 string,因为出勤率需要进行数学运算——在渲染柱状图时,柱子高度的计算公式为 (m.rate / 100 * 66).toFixed(0) + 'vp',必须依赖数值类型才能完成。
ArkTS 中 number 类型对应 JavaScript 的 Number 对象,支持所有标准数学运算。id 同样作为 ForEach 的键值生成器使用。这个接口虽然结构简单,但它承载了统计页面核心的可视化数据。在实际企业应用中,这些数据通常来自后端 API 接口返回的 JSON 数据,ArkTS 的 interface 定义可以确保前端在消费数据时拥有类型安全保障。
3.3 ShiftCell 排班单元接口
interface ShiftCell {
id: number
day: number
shift: string
color: string
}
排班单元接口定义了日历中每个格子(即每一天)的数据结构。day 是日期数字(1-30),对应月份中的第几天。shift 是班次标识,取值为 '早'(早班)、'晚'(晚班)或 '休'(休息日)。color 是该班次对应的颜色色值,如早班为 '#00897B'(青绿色)、晚班为 '#FB8C00'(橙色)、休息日为 '#B0BEC5'(灰色)。
这里将颜色直接内嵌在数据结构中是一种"数据驱动样式"的设计策略。当班次类型与颜色的映射关系需要调整时,只需修改数据源,UI 渲染会自动更新。这比在 UI 层使用 switch/case 条件判断颜色更加解耦和灵活。id 字段在排班日历的 ForEach 渲染中作为唯一键,确保日历格子在数据更新时能够精准定位和增量更新。
3.4 LeaveItem 请假记录接口
interface LeaveItem {
id: number
type: string
icon: string
days: string
status: string
reason: string
approver: string
}

请假记录接口是最复杂的数据结构之一,包含七个字段。type 是请假类型(年假、调休、病假、事假、婚假、产假/陪产),icon 是对应的 Emoji 图标(🏖️、🔄、🏥、📝、💒、👶),days 是请假天数(如 '5天'、'0.5天')。
status 字段是审批状态,取值为 '审批中'、'已通过'、'已驳回'、'未提交' 四种之一。这个字段直接驱动了 UI 层的状态标签颜色映射:'已通过' 显示绿色、'审批中' 显示橙色、'已驳回' 显示红色、'未提交' 显示灰色。reason 是请假事由,approver 是审批人信息(如 '直属主管·王莉'、'人事经理·陈静')。
这个接口的设计体现了企业审批流程的核心要素:类型分类、状态追踪、事由记录、审批人关联。在真实的企业级应用中,这些字段通常还包含创建时间、审批时间、审批意见等元数据,这里为了演示做了简化处理。
3.5 Colleague 同事接口
interface Colleague {
id: number
name: string
avatar: string
dept: string
free: boolean
}

同事接口用于换班申请场景中的同事列表展示。name 是同事姓名,avatar 是 Emoji 头像(如 '👩💼'、'🧑💻'),dept 是所属部门。free 是一个布尔值,表示该同事在换班目标日是否空闲。
boolean 类型在 ArkTS 中对应 JavaScript 的 boolean 原始类型,取值为 true 或 false。在 UI 渲染中,free 字段通过 if/else 条件分支决定显示 '当日空闲'(绿色标签)还是 '已有安排'(灰色标签)。这种布尔标志位驱动的条件渲染是 ArkUI 声明式范式中最常见的模式之一。同时,free 还会间接影响列表项的选中态样式,空闲的同事在视觉上更优先推荐给用户。
3.6 OverTimeLog 加班记录接口
interface OverTimeLog {
id: number
date: string
hours: number
pay: string
kind: string
}

加班记录接口定义了加班登记历史的数据结构。date 是加班日期(如 '08-26'),hours 是加班时长(number 类型,如 2、1.5、4),pay 是补偿金额(如 '¥172'),kind 是补偿方式('调休' 或 '加班费')。
hours 字段使用 number 类型而非 string,这是因为加班时长需要进行数学运算:在统计页面的加班记录列表中,每条记录旁有一个进度条,其宽度计算公式为 (o.hours / 4 * 40).toFixed(0) + 'vp'。这里的 4 是归一化基数(加班时长上限为4小时),40 是进度条最大宽度(单位 vp)。这种将数据值映射为视觉尺寸的做法是数据可视化的基础模式。
3.7 类型定义层的整体设计哲学
纵观六个接口定义,我们可以总结出几个共同的设计原则。第一,每个接口都包含 id 字段作为唯一标识,这不仅是数据管理的需要,更是 ArkUI ForEach 组件进行列表虚拟DOM diff 优化的关键依据。第二,所有字段的类型都是原始类型(number、string、boolean),没有嵌套对象或数组,保持了数据结构的扁平化。第三,用于 UI 展示的字段(如 icon、color、status)直接存储最终值或语义化标记,而非需要进一步转换的编码值,这简化了渲染逻辑。
四、数据层静态常量解析
4.1 CLOCK_LOGS 打卡记录数据
const CLOCK_LOGS: ClockLog[] = [
{ id: 1, date: '08-26 今天', on: '08:52', off: '--', state: '已上班', hours: '-' },
{ id: 2, date: '08-25 昨天', on: '08:48', off: '18:35', state: '正常', hours: '8.5h' },
// ...
]

const 关键字在 ArkTS 中声明一个不可重新赋值的常量。这里 CLOCK_LOGS 被定义为 ClockLog[] 数组类型,包含8条打卡记录。注意 const 只保证变量引用不可变,数组内部的对象属性仍然可以被修改(虽然在声明式UI范式中我们不会直接修改静态数据)。
第一条记录 id: 1 表示今天的打卡状态,off: '--' 表示尚未打下班卡,state: '已上班' 表示已打上班卡但全天考勤状态尚未最终确认。hours: '-' 表示工时暂未统计。第二条记录是昨天的完整打卡数据,上班 08:48、下班 18:35,状态为"正常",工时 8.5h。这种数据结构设计让列表渲染时可以自然地按时间倒序展示,最新记录在最顶部。
4.2 MONTH_RATE 月度出勤率数据
const MONTH_RATE: MonthRate[] = [
{ id: 1, month: '3月', rate: 96 },
{ id: 2, month: '4月', rate: 98 },
{ id: 3, month: '5月', rate: 94 },
{ id: 4, month: '6月', rate: 100 },
{ id: 5, month: '7月', rate: 97 },
{ id: 6, month: '8月', rate: 99 }
]

月度出勤率数据包含6个月的统计数据,出勤率范围在94%到100%之间。其中6月份达到满分100%,5月份最低为94%。这些数据将直接驱动统计页面的柱状图渲染——每根柱子的高度与 rate 值成正比,同时根据 rate 是否大于等于98来决定柱子的颜色(#00897B 深青色或 #4DB6AC 浅青色),形成视觉上的"达标/未达标"区分。
4.3 SHIFT_CELLS 排班日历数据
const SHIFT_CELLS: ShiftCell[] = [
{ id: 1, day: 1, shift: '早', color: '#00897B' },
{ id: 2, day: 2, shift: '早', color: '#00897B' },
{ id: 3, day: 3, shift: '休', color: '#B0BEC5' },
// ...共30天
]
排班日历数据覆盖了整个月的30天,每天的班次类型通过 shift 字段标识('早'、'晚'、'休'),并内嵌对应的 color 色值。这种"数据即样式"的设计使得排班日历的渲染逻辑极其简洁——Flex 容器中通过 ForEach 遍历,每个 Column 格子直接读取 c.color 作为背景色,无需任何条件判断。
排班模式呈现出明显的规律性:早班(青绿色)占多数(18天),晚班(橙色)分布在特定日期(6天),休息日(灰色)均匀穿插(6天)。这种排班数据在真实场景中通常由排班管理系统自动生成或由部门主管手动排定,前端通过 API 拉取后渲染。
4.4 LEAVE_LIST 请假记录数据
const LEAVE_LIST: LeaveItem[] = [
{ id: 1, type: '年假', icon: '🏖️', days: '5天', status: '审批中', reason: '国庆提前出行', approver: '直属主管·王莉' },
{ id: 2, type: '调休', icon: '🔄', days: '1天', status: '已通过', reason: '8月24日加班调休', approver: '直属主管·王莉' },
// ...共6条
]
请假记录数据展示了六种不同类型的请假申请,涵盖了审批中的、已通过的、已驳回的和未提交的全部四种状态。这种设计使得请假页面能够完整展示所有可能的状态标签和颜色映射,是典型的"演示型数据"设计策略。
approver 字段的设计值得关注——它将审批人姓名和职务拼接在一起(如 '直属主管·王莉'、'人事经理·陈静'),在 UI 展示时无需额外的格式化处理。在真实应用中,这个字段通常是 approver_id(审批人ID),前端再通过通讯录接口查询获得审批人信息,这里为了简化做了内嵌处理。
4.5 COLLEAGUE_LIST 同事数据
const COLLEAGUE_LIST: Colleague[] = [
{ id: 1, name: '周雨薇', avatar: '👩💼', dept: '产品部', free: true },
{ id: 2, name: '吴启铭', avatar: '🧑💻', dept: '研发部', free: false },
// ...共8位
]
同事数据包含8位同事信息,覆盖了产品部、研发部、设计部、运维部、培训部、市场部、行政部等多个部门。free 字段的分布是随机的(true/false混合),使得换班同事列表能够自然展示"空闲"和"已有安排"两种状态的标签。
avatar 字段使用 Emoji 图标作为头像,这是一种轻量化的头像方案。在生产环境中,头像通常使用图片 URL 或 Base64 编码的图片数据,配合 ArkUI 的 Image 组件渲染。Emoji 头像的优势是零网络请求、即用即显、无需占位图处理,非常适合演示和原型开发场景。
4.6 OVERTIME_LOGS 加班记录数据
const OVERTIME_LOGS: OverTimeLog[] = [
{ id: 1, date: '08-26', hours: 2, pay: '¥172', kind: '调休' },
{ id: 2, date: '08-24', hours: 1, pay: '¥86', kind: '调休' },
// ...共8条
]
加班记录数据包含8条历史记录,hours 字段的值从1到4不等,pay 字段展示了对应的补偿金额。从数据可以看出,加班费计算基准约为 ¥86/小时(2小时 ¥172,1小时 ¥86),这与"我的"页面加班登记弹窗中 this.otHours * 86 的计算逻辑完全一致,体现了数据与逻辑的一致性。
kind 字段区分了两种补偿方式:'调休'(以调休时长补偿)和 '加班费'(以加班费形式补偿)。这种区分在 UI 展示时通过不同颜色标签呈现,同时也会影响加班登记弹窗中的补偿金额计算方式。
五、Tab 导航系统设计
5.1 枚举类型与元数据定义
enum AttendTab { CLOCK, STAT, SHIFT, LEAVE, MINE }
enum 是 ArkTS 中用于定义枚举类型的关键字。AttendTab 定义了五个 Tab 的标识符:CLOCK(值为0)、STAT(值为1)、SHIFT(值为2)、LEAVE(值为3)、MINE(值为4)。枚举成员的值默认从0开始递增,也可以显式指定。
在 ArkTS 的声明式UI中,enum 常用于管理有限的离散状态集合。相比字符串常量,枚举类型有三大优势:编译期类型检查(拼写错误在编译时即可发现)、代码补全支持(IDE自动提示可选值)、语义清晰(AttendTab.CLOCK 比 0 或 'clock' 更具可读性)。
interface AttendTabMeta {
icon: string
label: string
}
const ATTEND_TABS: AttendTabMeta[] = [
{ icon: '🕐', label: '打卡' },
{ icon: '📊', label: '统计' },
{ icon: '🗓️', label: '排班' },
{ icon: '🌴', label: '请假' },
{ icon: '👤', label: '我的' }
]
AttendTabMeta 接口定义了 Tab 的元数据结构,包含 icon(Emoji图标)和 label(文本标签)两个字段。ATTEND_TABS 数组将五个 Tab 的元数据按 AttendTab 枚举的顺序排列,确保索引与枚举值一一对应。
这种"枚举标识 + 元数据数组"的设计模式是 ArkUI 中 Tab 导航的经典范式。它的优势在于:Tab 的视觉属性(图标、文本)集中管理在一处,新增或修改 Tab 只需修改元数据数组,无需改动渲染逻辑。ForEach 循环遍历 ATTEND_TABS 即可自动生成所有 Tab 按钮。
六、主入口组件 AttendClockApp 深度解析
6.1 组件声明与状态初始化
@Entry
@Component
struct AttendClockApp {
@State currentTab: AttendTab = AttendTab.CLOCK
@Entry 装饰器标记此组件为页面的入口组件。在 HarmonyOS ArkTS 中,一个页面文件中只能有一个组件被标记为 @Entry,它会被编译器识别为页面的根节点,自动挂载到页面的渲染树中。
@Component 装饰器标记 AttendClockApp 为一个自定义组件。自定义组件是 ArkUI 的基本构建单元,它封装了 UI 结构和内部状态,可以被其他组件引用和复用。struct 关键字定义一个结构体类型,在 ArkTS 中自定义组件必须使用 struct 声明。
@State 装饰器是 ArkUI 状态管理体系中最核心的装饰器之一。它声明 currentTab 为组件的内部状态变量,类型为 AttendTab 枚举,初始值为 AttendTab.CLOCK。当 currentTab 的值发生变化时,ArkUI 框架会自动触发 build() 方法中引用了 currentTab 的 UI 部分重新渲染。
6.2 @State 装饰器深度讲解
@State 装饰器的作用机制可以概括为"观察+响应"。ArkUI 框架在组件初始化时会对 @State 变量进行劫持(类似于 Vue 的响应式原理),当变量值被重新赋值时,框架会检测到变化并标记所有依赖该变量的 UI 节点为"脏节点",然后在下一帧的渲染周期中批量更新这些节点。
@State 的核心特性包括:第一,它只能在 @Component 或 @Entry 修饰的组件内部使用;第二,它声明的变量是组件私有的内部状态,不对外暴露;第三,对于基本类型(number、string、boolean)的变化可以精确检测,对于对象和数组的变化也能通过引用比较检测到。
在本组件中,currentTab 变量驱动的 UI 部分是内容区的条件渲染分支和底部 Tab 的高亮样式。当用户点击某个 Tab 按钮时,onClick 回调将 currentTab 重新赋值,框架自动更新两部分 UI:内容区切换渲染对应的内容组件,底部 Tab 按钮的高亮状态切换到新选中的 Tab。
6.3 build 方法与顶部头部
build() {
Column() {
// ===== 顶部头部 =====
Row() {
Column() {
Text('8月26日 星期三').fontSize(12).fontColor('#B2DFDB')
Text('准时宝 · 考勤').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('🏢').fontSize(22)
Text('星辰大厦-A座').fontSize(8).fontColor('#B2DFDB').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 14 })
.linearGradient({ angle: 135, colors: [['#00695C', 0], ['#00897B', 1]] })
build() 方法是每个 ArkUI 组件必须实现的方法,它返回组件的 UI 结构。在 ArkTS 中,build() 方法内部使用声明式 DSL 语法,以组件函数调用的方式描述 UI 树。最外层的 Column() 创建了一个纵向排列的容器组件。
Column 组件详解:Column 是 ArkUI 中最基础的线性布局容器之一,它将子元素沿垂直方向(主轴为纵向)依次排列。Column 的常用属性包括:alignItems(设置子元素在交叉轴/水平方向的对齐方式)、justifyContent(设置子元素在主轴/垂直方向的分布方式)、layoutWeight(按比例分配剩余空间)、width/height(设置宽高)、padding(设置内边距)、backgroundColor(设置背景色)、borderRadius(设置圆角)等。
在顶部头部中,外层 Row() 创建了一个横向排列的容器。
Row 组件详解:Row 是 ArkUI 的另一个核心线性布局容器,它将子元素沿水平方向(主轴为横向)依次排列。Row 的常用属性与 Column 类似,但主轴方向不同。Row 特别适合用于横向并排布局的场景,如顶部导航栏、卡片内的左右分栏、底部按钮组等。
头部左侧的 Column 包含日期文本和标题文本,使用 alignItems(HorizontalAlign.Start) 让子元素左对齐。layoutWeight(1) 使其占据 Row 中除右侧公司信息外的全部剩余空间。Text('8月26日 星期三') 设置字号为12,颜色为浅青色 '#B2DFDB'(在深色背景上保证可读性)。
Text('准时宝 · 考勤') 设置字号为17,字重为 FontWeight.Bold(粗体),颜色为白色 '#FFFFFF'。margin({ top: 2 }) 在标题与日期之间添加2vp的垂直间距。Text 组件是 ArkUI 中最基础的文本展示组件,支持 fontSize、fontColor、fontWeight、fontStyle、maxLines、textAlign 等丰富的文本样式属性。
linearGradient 属性为容器设置线性渐变背景。angle: 135 表示渐变角度为135度(从左上到右下),colors 数组定义了渐变色值和位置:['#00695C', 0] 表示起始色为深青色(位置0%),['#00897B', 1] 表示终止色为中青色(位置100%)。这种双色渐变营造了考勤主题的青绿色视觉风格。
6.4 内容区条件渲染
// ===== 内容区 =====
Column() {
if (this.currentTab === AttendTab.CLOCK) {
ClockInContent()
} else if (this.currentTab === AttendTab.STAT) {
AttendStatContent()
} else if (this.currentTab === AttendTab.SHIFT) {
ShiftTableContent()
} else if (this.currentTab === AttendTab.LEAVE) {
LeaveCenterContent()
} else {
AttendMineContent()
}
}
.layoutWeight(1).width('100%')
内容区是应用的核心区域,它通过 if/else 条件分支根据 this.currentTab 的值渲染对应的内容组件。在 ArkTS 声明式UI中,if/else 条件渲染是控制 UI 结构动态变化的基本手段。当条件表达式的值改变时,ArkUI 框架会自动销毁旧分支的组件并创建新分支的组件。
这里使用了五个自定义组件:ClockInContent()(打卡页)、AttendStatContent()(统计页)、ShiftTableContent()(排班页)、LeaveCenterContent()(请假页)、AttendMineContent()(我的页)。每个组件都是独立的 @Component,拥有自己的 build() 方法和内部状态。通过组件化拆分,主入口的 build() 方法保持简洁,各功能模块的 UI 逻辑相互隔离。
layoutWeight(1) 使内容区占据 Column 中除顶部头部和底部 Tab 外的全部剩余空间。width('100%') 使内容区宽度铺满父容器。
6.5 底部 Tab 栏
// ===== 底部 Tab =====
Row() {
ForEach(ATTEND_TABS, (t: AttendTabMeta, idx: number) => {
Column() {
Text(t.icon).fontSize(20)
Text(t.label).fontSize(9)
.fontColor(this.currentTab === idx ? '#00897B' : '#9E9E9E')
.margin({ top: 2 })
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 7, bottom: 7 })
.backgroundColor(this.currentTab === idx ? '#E0F2F1' : '#FFFFFF')
.borderRadius(10).margin({ left: 4, right: 4 })
.onClick(() => {
this.currentTab = idx as AttendTab
})
}, (t: AttendTabMeta) => t.label)
}
.width('100%').backgroundColor('#FFFFFF')
.border({ width: { top: 1 }, color: '#E0F2F1' })
}
.width('100%').height('100%').backgroundColor('#F4FAF9')
ForEach 组件详解:ForEach 是 ArkUI 中用于列表渲染的核心组件。它接收三个参数:数据源数组(ATTEND_TABS)、子项渲染函数((t, idx) => { ... })和键值生成函数((t) => t.label)。ForEach 会遍历数据源数组,为每个元素调用渲染函数生成对应的 UI 节点,并通过键值生成函数为每个节点分配唯一标识。
在底部 Tab 栏中,ForEach 遍历 ATTEND_TABS 数组(5个元素),为每个 Tab 生成一个 Column 容器,内含 Emoji 图标 Text 和标签文本 Text。渲染函数的第二个参数 idx 是数组索引(0-4),它被用于与 this.currentTab 进行比较,判断当前 Tab 是否为激活状态。
条件样式绑定:fontColor、fontWeight、backgroundColor 等样式属性使用了三元运算符进行条件绑定:this.currentTab === idx ? '#00897B' : '#9E9E9E'。当当前 Tab 等于该 Tab 的索引时,文字颜色为青绿色 '#00897B',否则为灰色 '#9E9E9E'。这种模式是 ArkUI 声明式范式中实现"选中态高亮"的标准做法。
onClick 事件回调中,this.currentTab = idx as AttendTab 将点击的 Tab 索引赋值给状态变量。as AttendTab 是 ArkTS 的类型断言语法,将 number 类型的 idx 断言为 AttendTab 枚举类型。赋值后,ArkUI 框架自动触发 build() 方法中所有依赖 currentTab 的 UI 部分重新渲染——内容区切换组件、底部 Tab 高亮状态更新。
键值生成函数 (t: AttendTabMeta) => t.label 使用 Tab 的标签文本作为唯一键。在实际应用中,如果数据源的项可能重复,应使用唯一性更强的字段(如 id)作为键值。这里 Tab 标签本身就是唯一的(打卡、统计、排班、请假、我的),因此使用 label 是合理的。
border({ width: { top: 1 }, color: '#E0F2F1' }) 为 Row 设置仅顶部边框(宽度1vp,颜色浅青色),在底部 Tab 栏与内容区之间形成视觉分隔线。border 属性支持分别设置四个方向的边框宽度和颜色,{ top: 1 } 表示只设置顶部边框。
七、打卡页 ClockInContent 组件深度剖析
7.1 组件状态声明
@Component
struct ClockInContent {
@State clocked: boolean = false
@State ringGrow: boolean = false
@State pressScale: number = 1.0
@State showFix: boolean = false
@State fixDate: string = '08-20 周四'
@State fixReason: string = ''
打卡页组件声明了六个 @State 状态变量。clocked 布尔值追踪用户是否已打卡,初始为 false(未打卡)。ringGrow 控制进度环动画的填充状态,初始为 false(进度为0)。pressScale 记录打卡按钮的缩放比例,用于实现按压弹跳动画效果。
showFix 控制补卡弹窗的显示/隐藏。fixDate 存储用户在补卡弹窗中选择的日期,初始值为 '08-20 周四'。fixReason 存储用户输入的补卡原因文本,初始为空字符串。这六个状态变量共同构成了打卡页的全部响应式状态空间。
7.2 @Builder 装饰器与弹窗遮罩
@Builder modalOverlay(onClose: () => void) {
Column() {
Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
onClose()
})
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(998)
}
@Builder 装饰器深度讲解:@Builder 是 ArkTS 中用于定义可复用 UI 片段的装饰器。与 @Component 不同,@Builder 修饰的函数不是一个独立组件,而是一段可被 build() 方法引用的 UI 构建逻辑。@Builder 函数可以接收参数(如这里的 onClose 回调函数),实现参数化的 UI 复用。
modalOverlay 是一个通用的弹窗遮罩构建器。它接收一个 onClose 回闭函数作为参数,构建一个覆盖全屏的半透明遮罩层。内层 Column 设置 width('100%').height('100%') 铺满全屏,backgroundColor('rgba(0,0,0,0.5)') 设置半透明黑色背景(alpha 通道透明度为0.5),onClick 回调调用 onClose 函数,实现点击遮罩关闭弹窗的交互效果。
外层 Column 设置 position({ x: 0, y: 0 }) 将遮罩定位到页面左上角原点,zIndex(998) 将遮罩层级设为998,确保它覆盖在页面内容之上但低于弹窗内容层(弹窗的 zIndex 为999)。这种"遮罩层 + 弹窗层"的双层 zIndex 控制是 ArkUI 弹窗交互的经典模式。
7.3 补卡申请弹窗
@Builder fixModal() {
Column() {
Column() {
Row() {
Text('🩹').fontSize(18)
Text('补卡申请').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').margin({ left: 6 })
Text('本月剩 2 次').fontSize(9).fontColor('#FB8C00').margin({ left: 8 })
}
.width('100%').padding({ top: 16, left: 16 })
补卡弹窗是一个居中显示的表单弹窗。外层 Column 设置 alignItems(HorizontalAlign.Center) 使弹窗内容水平居中,position({ y: '16%' }) 将弹窗垂直定位在距顶部16%的位置,zIndex(999) 确保弹窗位于遮罩层之上。
内层 Column 设置 width('88%') 使弹窗宽度占屏幕的88%,backgroundColor('#FFFFFF') 设置白色背景,borderRadius(16) 设置16vp圆角。弹窗顶部的 Row 包含三个元素:Emoji图标、标题文本"补卡申请"和剩余次数提示"本月剩 2 次"。
Column() {
Text('补卡日期').fontSize(11).fontColor('#757575')
Row() {
ForEach(['08-19 周三', '08-20 周四', '08-21 周五'], (d: string) => {
Text(d).fontSize(10)
.fontColor(this.fixDate === d ? '#FFFFFF' : '#616161')
.backgroundColor(this.fixDate === d ? '#00897B' : '#F5F5F5')
.borderRadius(10).padding({ left: 10, right: 10, top: 5, bottom: 5 })
.margin({ right: 8 })
.onClick(() => {
this.fixDate = d
})
}, (d: string) => d)
}
.margin({ top: 6 })
}
日期选择区域使用 ForEach 渲染三个可选日期标签。每个日期标签是一个 Text 组件,通过条件判断 this.fixDate === d 来决定选中态样式:选中时文字为白色、背景为青绿色;未选中时文字为深灰色、背景为浅灰色。onClick 回调将点击的日期赋值给 this.fixDate,触发条件样式重新渲染。
这种"标签式单选"是 ArkUI 中处理有限选项选择的经典模式。相比下拉选择器(Select 组件),标签式选择器的所有选项一目了然,点击即选,交互效率更高,特别适合选项数量少(3-5个)的场景。
Column() {
Text('补卡原因').fontSize(11).fontColor('#757575')
TextArea({ placeholder: '例如:地铁故障导致迟到 15 分钟', text: this.fixReason })
.fontSize(12).height(70).backgroundColor('#F5F5F5').borderRadius(10)
.onChange((v: string) => {
this.fixReason = v
})
.margin({ top: 6 })
}
TextArea 是 ArkUI 中的多行文本输入组件。它接收一个配置对象参数,placeholder 设置占位提示文本,text 设置初始文本内容(绑定到 this.fixReason 状态变量)。onChange 回调在用户输入时被调用,参数 v 是最新的输入文本,将其赋值给 this.fixReason 实现双向数据绑定。
TextArea 与单行 TextInput 的区别在于:TextArea 支持多行文本输入,适合较长的文本内容(如请假事由、补卡原因);TextInput 只支持单行输入,适合短文本(如姓名、工号)。height(70) 将输入区域高度设为70vp,约可显示3-4行文本。
Row() {
Button() {
Text('取消').fontSize(14).fontColor('#616161')
}
.layoutWeight(1).height(38).backgroundColor('#F5F5F5').borderRadius(19)
.onClick(() => {
this.showFix = false
})
Button() {
Text('提交审批').fontSize(14).fontColor('#FFFFFF')
}
.layoutWeight(1.4).height(38).backgroundColor('#00897B').borderRadius(19)
.margin({ left: 10 })
.onClick(() => {
this.showFix = false
})
}
按钮组使用 Row 横向排列两个 Button 组件。Button 组件支持自定义子内容——这里通过 Button() { Text('取消') } 的方式将 Text 组件作为按钮内容。layoutWeight(1) 和 layoutWeight(1.4) 使两个按钮按 1:1.4 的比例分配水平空间,"提交审批"按钮更宽,突出主操作。
height(38) 和 borderRadius(19) 组合实现了高度38vp的全圆角按钮(圆角半径=高度的一半=19vp)。backgroundColor('#F5F5F5') 为取消按钮设置浅灰色背景,backgroundColor('#00897B') 为提交按钮设置品牌青绿色背景。两个按钮的 onClick 回调都将 this.showFix 设为 false,关闭弹窗。
7.4 打卡圆环与动画特效
build() {
Stack() {
Scroll() {
Column() {
Column() {
Stack() {
Progress({ value: this.ringGrow ? 62 : 0, total: 100, type: ProgressType.Ring })
.width(150).height(150)
.color('#00897B').backgroundColor('#E0F2F1')
.style({ strokeWidth: 10 })
.animation({ duration: 900, curve: Curve.EaseOut })
Column() {
Text('18:42').fontSize(26).fontWeight(FontWeight.Bold)
.fontColor(this.clocked ? '#00897B' : '#212121')
Text(this.clocked ? '今日工时 8.5h' : '未打上班卡').fontSize(10).fontColor('#9E9E9E').margin({ top: 4 })
Column() {
Text(this.clocked ? '✓ 已打卡' : '上班打卡').fontSize(15)
.fontColor(this.clocked ? '#FFFFFF' : '#00897B')
.fontWeight(FontWeight.Bold)
}
.width(92).height(92).borderRadius(46)
.backgroundColor(this.clocked ? '#00897B' : '#FFFFFF')
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
.margin({ top: 10 })
.animation({ duration: 200, curve: Curve.EaseOut })
.shadow({ radius: 6, color: 'rgba(0,137,123,0.25)', offsetY: 3 })
.onClick(() => {
this.pressScale = 1.12
setTimeout(() => {
this.pressScale = 1.0
this.clocked = true
this.ringGrow = true
}, 200)
})
}
.alignItems(HorizontalAlign.Center)
}
.width(170).height(170)
.margin({ top: 24 })
Stack 组件深度讲解:Stack 是 ArkUI 的堆叠布局容器,它将所有子元素在 Z 轴方向上堆叠排列——后声明的子元素覆盖在先声明的子元素之上。Stack 的常用属性包括:alignContent(设置子元素的对齐方式,默认居中)、width/height(设置宽高)、zIndex(设置子元素的堆叠层级)。
在打卡圆环中,Stack 包含两层:底层是 Progress 进度环组件,上层是包含时间文本和打卡按钮的 Column。Progress 组件的 value 属性通过三元运算符 this.ringGrow ? 62 : 0 绑定到状态变量——当 ringGrow 为 true 时进度填充到62%,为 false 时进度为0。
Progress 组件详解:Progress 是 ArkUI 的进度展示组件,支持三种类型:ProgressType.Linear(线性进度条)、ProgressType.Ring(环形进度)和 ProgressType.Eclipse(月食进度)。这里使用 ProgressType.Ring 创建环形进度。total: 100 设置进度总值为100,value 为当前进度值。color 设置已填充进度部分的颜色,backgroundColor 设置未填充部分的背景色。style({ strokeWidth: 10 }) 设置进度环的线宽为10vp。
animation({ duration: 900, curve: Curve.EaseOut }) 修饰器为 Progress 组件绑定动画效果。当 value 值发生变化时(从0变为62),进度环会在900毫秒内以 EaseOut(先快后慢)曲线完成填充动画。Curve.EaseOut 是 ArkUI 内置的缓动曲线之一,其他常用曲线包括 Curve.Linear(匀速)、Curve.EaseIn(先慢后快)、Curve.EaseInOut(两端慢中间快)。
打卡按钮内部 Column 的样式通过 this.clocked 状态进行条件绑定:未打卡时背景为白色、文字为青绿色"上班打卡";已打卡时背景为青绿色、文字为白色"✓ 已打卡"。width(92).height(92).borderRadius(46) 创建了一个92vp的正圆形按钮(圆角半径=宽度/2)。
shadow({ radius: 6, color: 'rgba(0,137,123,0.25)', offsetY: 3 }) 为按钮添加投影效果。radius 是模糊半径,color 是阴影颜色(半透明青绿色),offsetY: 3 使阴影在垂直方向偏移3vp,营造立体悬浮感。
打卡按钮的 onClick 回调实现了一个两阶段的按压弹跳动画。第一阶段立即将 this.pressScale 设为1.12(放大12%),模拟按钮被按下的效果。第二阶段通过 setTimeout 延迟200毫秒后,将 pressScale 恢复为1.0,同时将 clocked 设为 true(标记已打卡),ringGrow 设为 true(触发进度环动画)。这种"先缩放反馈、后状态变更"的动画设计模拟了真实物理按钮的按压弹性反馈。
7.5 快捷操作区域
Row() {
Column() {
Text('🩹').fontSize(22)
Text('补卡').fontSize(10).fontColor('#616161').margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 12, bottom: 12 }).margin({ left: 12, right: 5 })
.onClick(() => {
this.showFix = true
})
Column() {
Text('⏱️').fontSize(22)
Text('加班').fontSize(10).fontColor('#616161').margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 12, bottom: 12 }).margin({ left: 5, right: 5 })
Column() {
Text('🔄').fontSize(22)
Text('换班').fontSize(10).fontColor('#616161').margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 12, bottom: 12 }).margin({ left: 5, right: 5 })
Column() {
Text('📍').fontSize(22)
Text('外勤').fontSize(10).fontColor('#616161').margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 12, bottom: 12 }).margin({ left: 5, right: 12 })
}
.width('100%').margin({ top: 18 })
快捷操作区域使用 Row 横向排列四个功能入口:补卡、加班、换班、外勤。每个入口是一个 Column 容器,内含 Emoji 图标和文字标签。layoutWeight(1) 使四个入口等宽分配空间。margin 的左右间距通过 left 和 right 参数精细控制——最左边的入口 left: 12,最右边的入口 right: 12,中间的入口各5vp,形成均匀的间距分布。
只有"补卡"入口绑定了 onClick 事件(将 this.showFix 设为 true 以打开补卡弹窗),其他三个入口(加班、换班、外勤)仅作展示。在实际应用中,这些入口需要绑定各自的功能逻辑,如跳转到加班登记页面、发起换班流程等。
7.6 打卡记录列表
Column() {
Text('📋 近期打卡记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ left: 14, top: 12 })
ForEach(CLOCK_LOGS, (l: ClockLog, i: number) => {
Column() {
Row() {
Column() {
Text(l.date).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
Text('上 ' + l.on + ' · 下 ' + l.off).fontSize(9).fontColor('#9E9E9E').margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(l.hours).fontSize(11).fontColor('#616161').margin({ right: 10 })
Text(l.state).fontSize(9)
.fontColor(l.state === '迟到' ? '#F44336' : l.state === '正常' || l.state === '已上班' ? '#43A047' : l.state === '加班' ? '#FB8C00' : '#9E9E9E')
.backgroundColor(l.state === '迟到' ? '#FFEBEE' : l.state === '正常' || l.state === '已上班' ? '#E8F5E9' : l.state === '加班' ? '#FFF3E0' : '#F5F5F5')
.borderRadius(8).padding({ left: 8, right: 8, top: 3, bottom: 3 })
}
.width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
if (i < CLOCK_LOGS.length - 1) {
Divider().color('#F5F5F5')
}
}
.width('100%')
}, (l: ClockLog) => l.id.toString())
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 12, bottom: 20 })
打卡记录列表使用 ForEach 遍历 CLOCK_LOGS 数组(8条记录)。每条记录是一个 Column 容器,内含一个 Row(记录主体)和一个条件渲染的 Divider 分割线。
记录主体的 Row 包含三部分:左侧的日期和打卡时间 Column(layoutWeight(1) 占据剩余空间)、中间的工时 Text、右侧的状态标签 Text。状态标签的颜色和背景色通过嵌套三元运算符实现"状态-颜色"映射:迟到为红色系、正常/已上班为绿色系、加班为橙色系、其他为灰色系。
if (i < CLOCK_LOGS.length - 1) {
Divider().color('#F5F5F5')
}
Divider 是 ArkUI 的分割线组件。这里使用条件渲染 if (i < CLOCK_LOGS.length - 1) 确保分割线只出现在非最后一项的记录之间,避免列表末尾出现多余的分割线。这种"条件分割线"模式是 ArkUI 列表渲染中的常见技巧。
7.7 弹窗渲染与页面挂载
if (this.showFix) {
this.modalOverlay(() => {
this.showFix = false
})
this.fixModal()
}
}
.width('100%').height('100%')
.onAppear(() => {
this.ringGrow = true
})
}
}
弹窗的显示通过条件渲染 if (this.showFix) 控制。当 showFix 为 true 时,同时渲染遮罩层(this.modalOverlay)和弹窗内容(this.fixModal)。注意这里使用 this. 前缀调用 @Builder 函数,这是 ArkTS 中引用组件内 @Builder 方法的语法要求。
onAppear 生命周期详解:onAppear 是 ArkUI 组件的生命周期回调,在组件首次挂载到渲染树时触发。这里在页面出现时立即将 ringGrow 设为 true,触发进度环的填充动画。配合 Progress 组件上绑定的 animation 修饰器,用户打开打卡页时会看到进度环从0%平滑填充到62%的入场动画效果。
7.8 打卡页数据流向图
八、统计页 AttendStatContent 组件深度剖析
8.1 组件状态与数据总览
@Component
struct AttendStatContent {
@State barAnim: boolean = false
统计页只声明了一个状态变量 barAnim,用于控制柱状图的动画状态。初始值为 false(柱子高度为1vp),在 onAppear 生命周期中延迟150毫秒后设为 true,触发柱状图从1vp增长到目标高度的动画效果。
Row() {
Column() {
Text('20').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#00897B')
Text('出勤天数').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(32).backgroundColor('#E0E0E0')
Column() {
Text('17h').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FB8C00')
Text('累计加班').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(32).backgroundColor('#E0E0E0')
Column() {
Text('1').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F44336')
Text('迟到次数').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(32).backgroundColor('#E0E0E0')
Column() {
Text('99%').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#212121')
Text('出勤率').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 16, bottom: 16 }).margin({ left: 12, right: 12, top: 10 })
数据总览区域使用 Row 横向排列四个统计指标:出勤天数(20天,青绿色)、累计加班(17h,橙色)、迟到次数(1次,红色)、出勤率(99%,深灰色)。每个指标是一个 Column,内含数值 Text 和标签 Text。
指标之间使用 Column().width(1).height(32).backgroundColor('#E0E0E0') 作为竖直分割线——一个宽度1vp、高度32vp、浅灰色的色块。这种"色块模拟分割线"的技巧在 ArkUI 中很常见,因为 Divider 组件默认是水平分割线,竖直分割需要使用色块替代。
8.2 月度出勤率柱状图
Column() {
Text('📊 近6个月出勤率').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ left: 14, top: 12 })
Row() {
ForEach(MONTH_RATE, (m: MonthRate) => {
Column() {
Text(m.rate.toString() + '%').fontSize(8).fontColor('#00897B')
Column()
.width(18)
.height(this.barAnim ? (m.rate / 100 * 66).toFixed(0) + 'vp' : '1vp')
.backgroundColor(m.rate >= 98 ? '#00897B' : '#4DB6AC')
.borderRadius({ topLeft: 4, topRight: 4 })
.animation({ duration: 600, curve: Curve.EaseOut })
Text(m.month).fontSize(8).fontColor('#9E9E9E').margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (m: MonthRate) => m.id.toString())
}
.width('100%').padding({ left: 14, right: 14, top: 14, bottom: 14 })
.alignItems(VerticalAlign.Bottom)
.height(100)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 10 })
月度出勤率柱状图是统计页的核心可视化模块。Row 容器通过 ForEach 遍历 MONTH_RATE 数组(6个月数据),为每个月渲染一个 Column 柱子。Row 设置 alignItems(VerticalAlign.Bottom) 使所有柱子底部对齐,height(100) 固定图表区域高度为100vp。
每个柱子是一个 Column,从上到下包含三个元素:出勤率百分比标签 Text、柱体 Column(纯色块)、月份标签 Text。柱体的高度通过表达式 this.barAnim ? (m.rate / 100 * 66).toFixed(0) + 'vp' : '1vp' 动态计算——动画触发前高度为1vp(几乎不可见),动画触发后高度为 (rate/100*66) vp(最大66vp)。
(m.rate / 100 * 66).toFixed(0) + 'vp' 这一行表达式是柱状图高度计算的核心。m.rate 是出勤率(0-100),除以100得到比例值,乘以66得到以vp为单位的像素高度,.toFixed(0) 取整,最后拼接 'vp' 字符串作为单位后缀。例如出勤率96%对应高度 (96/100*66).toFixed(0) = '63vp',出勤率100%对应高度 '66vp'。
柱体颜色通过 m.rate >= 98 ? '#00897B' : '#4DB6AC' 条件判断——出勤率大于等于98%的月份使用深青色,低于98%的月份使用浅青色,形成"达标/未达标"的视觉区分。borderRadius({ topLeft: 4, topRight: 4 }) 只设置柱子顶部圆角,模拟真实柱状图的圆顶效果。
animation({ duration: 600, curve: Curve.EaseOut }) 修饰器绑定了600毫秒的缓出动画。当 barAnim 从 false 变为 true 时,每个柱子的高度从1vp平滑增长到目标值,所有柱子同时开始动画,形成集体"生长"的入场效果。
8.3 加班记录列表
Column() {
Text('⏱️ 加班记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ left: 14, top: 12 })
ForEach(OVERTIME_LOGS, (o: OverTimeLog, i: number) => {
Row() {
Column() {
Text(o.date).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
Text(o.kind).fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Row() {
Column()
.width((o.hours / 4 * 40).toFixed(0) + 'vp')
.height(6).backgroundColor('#FB8C00').borderRadius(3)
}
.width(40).height(6).backgroundColor('#FFF3E0').borderRadius(3)
.margin({ right: 10 })
Text(o.hours.toString() + 'h').fontSize(10).fontColor('#FB8C00').width(26).textAlign(TextAlign.End)
Text(o.pay).fontSize(10).fontColor('#616161').width(44).textAlign(TextAlign.End)
}
.width('100%').padding({ left: 14, right: 14, top: 10, bottom: 10 })
if (i < OVERTIME_LOGS.length - 1) {
Divider().color('#F5F5F5')
}
}, (o: OverTimeLog) => o.id.toString())
}
加班记录列表使用 ForEach 遍历 OVERTIME_LOGS 数组(8条记录)。每条记录的 Row 包含四部分:左侧的日期和补偿方式 Column、中间的进度条 Row、加班时长 Text、补偿金额 Text。
进度条是一个双层结构:外层 Row 宽度40vp、高度6vp、背景浅橙色,是进度条容器;内层 Column 宽度为 (o.hours / 4 * 40).toFixed(0) + 'vp',高度6vp,背景深橙色,是已填充的进度部分。(o.hours / 4 * 40) 将加班时长归一化(最大4小时对应40vp宽度),再转换为以vp为单位的宽度字符串。
textAlign(TextAlign.End) 使加班时长和补偿金额文本右对齐,形成数字列表的右边缘对齐效果。TextAlign 枚举提供三种对齐方式:Start(左对齐)、Center(居中)、End(右对齐)。
8.4 部门出勤排行
Column() {
Text('🏆 部门出勤排行').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ left: 14, top: 12 })
ForEach(['研发部 99.2%', '产品部 98.8%', '设计部 98.5%', '市场部 97.9%', '行政部 97.6%'], (d: string, i: number) => {
Row() {
Text((i + 1).toString()).fontSize(11).fontWeight(FontWeight.Bold)
.fontColor(i < 3 ? '#FB8C00' : '#9E9E9E').width(20)
Text(d.split(' ')[0]).fontSize(12).fontColor('#212121').layoutWeight(1)
Text(d.split(' ')[1]).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#00897B')
}
.width('100%').padding({ left: 14, right: 14, top: 9, bottom: 9 })
}, (d: string) => d)
}
部门出勤排行使用 ForEach 遍历一个字符串数组(5个部门及其出勤率)。每行的 Row 包含三部分:排名序号 Text(前3名橙色,4-5名灰色)、部门名称 Text(使用 d.split(' ')[0] 从字符串中提取)、出勤率 Text(使用 d.split(' ')[1] 提取,青绿色粗体)。
d.split(' ') 是 JavaScript/ArkTS 标准的字符串分割方法,将字符串 '研发部 99.2%' 按空格分割为 ['研发部', '99.2%'] 数组,然后通过索引 [0] 和 [1] 分别获取部门名称和出勤率。这种"内联数据解析"虽然方便,但在实际应用中建议使用结构化数据(如对象数组),避免字符串解析带来的脆弱性。
排名序号的颜色通过 i < 3 ? '#FB8C00' : '#9E9E9E' 条件判断——前3名(索引0-2)使用橙色突出显示,4-5名使用灰色。width(20) 固定排名区域的宽度为20vp,确保部门名称左对齐。
8.5 统计页渲染流程图
九、排班页 ShiftTableContent 组件深度剖析
9.1 组件状态声明
@Component
struct ShiftTableContent {
@State showDelete: boolean = false
@State showExchange: boolean = false
@State pickedColleague: number = 0
排班页声明了三个状态变量。showDelete 控制删除排班警示弹窗的显示/隐藏。showExchange 控制换班申请弹窗的显示/隐藏。pickedColleague 存储用户在换班弹窗中选择的同事ID,初始值为0(未选择)。
三个状态变量驱动了排班页的两个弹窗交互。showDelete 和 showExchange 是布尔开关量,pickedColleague 是数值标识符。当用户在换班弹窗中点击某个同事时,pickedColleague 更新为该同事的ID,触发选中态样式(边框变色、背景填充)的重新渲染。
9.2 图例区域
Scroll() {
Column() {
Row() {
Row() {
Column().width(10).height(10).borderRadius(5).backgroundColor('#00897B')
Text('早班 09:00-18:00').fontSize(9).fontColor('#616161').margin({ left: 5 })
}
.margin({ right: 14 })
Row() {
Column().width(10).height(10).borderRadius(5).backgroundColor('#FB8C00')
Text('晚班 13:00-22:00').fontSize(9).fontColor('#616161').margin({ left: 5 })
}
.margin({ right: 14 })
Row() {
Column().width(10).height(10).borderRadius(5).backgroundColor('#B0BEC5')
Text('休息日').fontSize(9).fontColor('#616161').margin({ left: 5 })
}
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ top: 12 })
图例区域使用 Row 横向排列三个图例项,每个图例项是一个嵌套 Row,包含一个色块 Column 和说明文字 Text。色块通过 width(10).height(10).borderRadius(5).backgroundColor(...) 创建一个10vp的正圆点(圆角半径=宽度/2=5vp),颜色对应班次类型。
justifyContent(FlexAlign.Center) 使三个图例项整体在 Row 中水平居中。FlexAlign 枚举提供六种主轴分布方式:Start(起始端对齐)、Center(居中)、End(末端对齐)、SpaceBetween(两端对齐无间距)、SpaceAround(等距环绕)、SpaceEvenly(等距均匀分布)。
9.3 排班日历网格
Column() {
Row() {
ForEach(['一', '二', '三', '四', '五', '六', '日'], (w: string) => {
Text(w).fontSize(9).fontColor('#9E9E9E')
.layoutWeight(1).textAlign(TextAlign.Center).padding({ top: 8, bottom: 4 })
}, (w: string) => w)
}
.width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(SHIFT_CELLS, (c: ShiftCell) => {
Column() {
Text(c.day.toString()).fontSize(11).fontColor('#212121')
Text(c.shift).fontSize(10).fontWeight(FontWeight.Bold)
.fontColor(c.shift === '休' ? '#FFFFFF' : '#FFFFFF')
.margin({ top: 2 })
}
.width('13.4%').height(44)
.backgroundColor(c.color).borderRadius(8)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
.margin(2)
.onClick(() => {
if (c.shift !== '休') {
this.showExchange = true
}
})
}, (c: ShiftCell) => c.id.toString())
}
.width('100%').padding({ left: 4, right: 4, bottom: 10 })
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 10 })
排班日历是排班页的核心模块。星期表头使用 Row 和 ForEach 渲染七个星期标签(一至日),每个标签 layoutWeight(1) 等宽分布。
Flex 组件深度讲解:Flex 是 ArkUI 中更灵活的弹性布局容器,它的功能是 Row 和 Column 的超集。Flex 接收一个配置对象参数,wrap: FlexWrap.Wrap 设置子元素自动换行模式——当一行排满后自动换到下一行。这是日历网格的关键布局属性,因为30天需要分成约5行(每行7天,最后一天单独一行)。
FlexWrap 枚举提供三种换行模式:NoWrap(不换行,子元素可能溢出)、Wrap(正向换行)、WrapReverse(反向换行)。日历场景使用 Wrap 模式,元素按从左到右、从上到下的顺序排列。
每个日期格子是一个 Column 容器,width('13.4%') 使宽度约为屏幕的13.4%(7个格子约94%,加上margin间距后正好铺满一行)。height(44) 固定格子高度为44vp。backgroundColor(c.color) 直接从数据中读取颜色值作为背景——这就是我们在类型定义中将 color 内嵌在 ShiftCell 中的设计意图体现。
格子的 onClick 回调包含条件逻辑:只有非休息日(c.shift !== '休')的格子才响应点击,点击后打开换班申请弹窗。休息日格子不响应点击,避免用户对休息日发起换班操作。
9.4 班次统计区域
Row() {
Column() {
Text('18').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#00897B')
Text('早班').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('6').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FB8C00')
Text('晚班').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('6').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#9E9E9E')
Text('休息').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('±0').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
Text('调班次数').fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.padding({ top: 12, bottom: 12 }).margin({ left: 12, right: 12, top: 10 })
班次统计区域使用 Row 横向排列四个统计指标:早班18天(青绿色)、晚班6天(橙色)、休息6天(灰色)、调班次数0次(深灰色)。每个指标使用 layoutWeight(1) 等宽分布,alignItems(HorizontalAlign.Center) 使内容居中对齐。
9.5 删除排班警示弹窗
@Builder deleteShiftModal() {
Column() {
Column() {
Text('🗑️').fontSize(34).margin({ top: 18 })
Text('删除 8月30日 早班排班?').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
.margin({ top: 10 })
Text('删除后该日将显示为空班,\n需要主管二次确认方可生效。')
.fontSize(11).fontColor('#9E9E9E').textAlign(TextAlign.Center).margin({ top: 8 })
Row() {
Button() {
Text('取消').fontSize(14).fontColor('#616161')
}
.layoutWeight(1).height(38).backgroundColor('#F5F5F5').borderRadius(19)
.onClick(() => {
this.showDelete = false
})
Button() {
Text('删除排班').fontSize(14).fontColor('#FFFFFF')
}
.layoutWeight(1).height(38).backgroundColor('#F44336').borderRadius(19)
.margin({ left: 10 })
.onClick(() => {
this.showDelete = false
})
}
.width('100%').padding({ left: 18, right: 18, top: 18, bottom: 18 })
}
.width('78%').backgroundColor('#FFFFFF').borderRadius(16)
}
.width('100%').alignItems(HorizontalAlign.Center)
.position({ y: '30%' }).zIndex(999)
}
删除排班弹窗是一个警示风格的居中弹窗。弹窗宽度为屏幕的78%(比补卡弹窗的88%更窄,营造警示弹窗的紧凑感)。弹窗通过 position({ y: '30%' }) 定位在距顶部30%的位置,zIndex(999) 确保位于遮罩层之上。
弹窗顶部使用大号 Emoji 🗑️(fontSize 34)作为视觉警示图标,紧接着是加粗的确认标题和说明文字。说明文字中使用了 \n 换行符,配合 textAlign(TextAlign.Center) 实现居中换行文本的展示。
按钮组的"删除排班"按钮使用红色背景 '#F44336',与补卡弹窗的青绿色提交按钮形成颜色语义对比——红色代表危险操作,绿色代表安全操作。这种"颜色语义化"设计在交互设计中非常重要,它通过视觉直觉提示用户操作的后果严重程度。
9.6 换班申请同事选择弹窗
@Builder exchangeModal() {
Column() {
Column() {
Text('🔄 发起换班申请').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ top: 16, left: 16 })
Text('将 8月27日 早班 与以下同事交换:').fontSize(10).fontColor('#9E9E9E')
.width('100%').padding({ left: 16, top: 6 })
Scroll() {
Column() {
ForEach(COLLEAGUE_LIST, (c: Colleague) => {
Row() {
Text(c.avatar).fontSize(26)
.width(40).height(40).backgroundColor('#E0F2F1').borderRadius(20)
.textAlign(TextAlign.Center)
Column() {
Text(c.name).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(this.pickedColleague === c.id ? '#00897B' : '#212121')
Row() {
Text(c.dept).fontSize(9).fontColor('#9E9E9E')
if (c.free) {
Text('当日空闲').fontSize(8).fontColor('#43A047')
.backgroundColor('#E8F5E9').borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ left: 6 })
} else {
Text('已有安排').fontSize(8).fontColor('#9E9E9E')
.backgroundColor('#F5F5F5').borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ left: 6 })
}
}
.margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
Column()
.width(18).height(18).borderRadius(9)
.border({ width: 2, color: this.pickedColleague === c.id ? '#00897B' : '#E0E0E0' })
.backgroundColor(this.pickedColleague === c.id ? '#00897B' : '#FFFFFF')
}
.width('100%').padding({ left: 16, right: 16, top: 9, bottom: 9 })
.onClick(() => {
this.pickedColleague = c.id
})
}, (c: Colleague) => c.id.toString())
}
.constraintSize({ maxHeight: 240 })
}
.scrollBar(BarState.Off)
.margin({ top: 8 })
换班申请弹窗是排班页最复杂的弹窗,它包含一个可滚动的同事列表。弹窗标题和说明文字后是一个 Scroll 组件,内部使用 ForEach 遍历 COLLEAGUE_LIST 数组(8位同事)。
Scroll 组件深度讲解:Scroll 是 ArkUI 的可滚动容器组件,它使其子内容可以在超出容器尺寸时进行滚动查看。Scroll 的常用属性包括:scrollBar(控制滚动条显示状态)、scrollDirection(设置滚动方向,默认纵向)、edgeEffect(设置边缘滑动效果)。这里使用 scrollBar(BarState.Off) 隐藏滚动条,使列表外观更简洁。
constraintSize({ maxHeight: 240 }) 为 Scroll 内部的 Column 设置最大高度约束为240vp。当同事列表内容超过这个高度时,内容区域变为可滚动状态。这种"最大高度约束 + 滚动"的模式是 ArkUI 中处理不定长列表的标准做法,确保弹窗不会因列表内容过多而超出屏幕范围。
每位同事的列表项是一个 Row,从左到右包含:头像 Text(Emoji,40x40圆形背景)、同事信息 Column(姓名+部门+空闲标签)、选中指示器 Column(圆形复选框)。
空闲标签使用 if/else 条件渲染:c.free 为 true 时显示绿色"当日空闲"标签,为 false 时显示灰色"已有安排"标签。这种条件渲染使每个列表项根据自身数据呈现不同的视觉信息。
选中指示器是一个18x18vp的圆形 Column,通过 border 和 backgroundColor 的条件绑定实现单选效果:选中时边框和背景都为青绿色(实心圆),未选中时边框为浅灰色、背景为白色(空心圆)。pickedColleague === c.id 是选中判断条件,onClick 回调将 pickedColleague 设为被点击同事的ID,触发所有列表项的选中态样式重新渲染。
Row() {
Button() {
Text('取消').fontSize(14).fontColor('#616161')
}
.layoutWeight(1).height(38).backgroundColor('#F5F5F5').borderRadius(19)
.onClick(() => {
this.showExchange = false
})
Button() {
Text('发送申请').fontSize(14).fontColor('#FFFFFF')
}
.layoutWeight(1.4).height(38).backgroundColor('#00897B').borderRadius(19)
.margin({ left: 10 })
.onClick(() => {
this.showExchange = false
})
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 16 })
}
.width('88%').backgroundColor('#FFFFFF').borderRadius(16)
}
.width('100%').alignItems(HorizontalAlign.Center)
.position({ y: '12%' }).zIndex(999)
}
换班弹窗的按钮组与补卡弹窗类似,"取消"按钮灰色、"发送申请"按钮青绿色,layoutWeight 比例为1:1.4。两个按钮的 onClick 回调都将 showExchange 设为 false,关闭弹窗。
9.7 班次说明列表
Column() {
Text('📋 班次说明').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
.width('100%').padding({ left: 14, top: 12 })
ForEach(['早班:09:00 - 18:00,午休 12:00-13:30', '晚班:13:00 - 22:00,晚补 ¥25/天', '换班需提前 48 小时申请,双人确认', '月度排班由主管在每月 25 日发布'], (r: string, i: number) => {
Row() {
Text('·').fontSize(14).fontColor('#00897B')
Text(r).fontSize(10).fontColor('#616161').margin({ left: 8 })
}
.width('100%').padding({ left: 14, right: 14, top: 7, bottom: 7 })
}, (r: string) => r)
}
.width('100%').backgroundColor('#FFFFFF').borderRadius(12)
.margin({ left: 12, right: 12, top: 12, bottom: 20 })
}
}
.width('100%').height('100%')
}
}

21.6 总结:
本应用充分利用了 HarmonyOS 6.1.1 基于 API 24 提供的多项 ArkUI 能力。Progress 组件的 ProgressType.Ring 环形进度模式和 animation 动画修饰器实现了打卡圆环的平滑填充效果。linearGradient 属性的双色渐变背景为头部和卡片营造了考勤主题的视觉基调。Flex 组件的 FlexWrap.Wrap 自动换行模式使排班日历的30天格子实现了自动排列。Stack 的 zIndex 精确控制实现了弹窗遮罩层和内容层的堆叠管理。
Scroll 组件的 scrollBar(BarState.Off) 和 constraintSize 配合实现了隐藏滚动条的可滚动列表。TextArea 组件的 onChange 双向绑定实现了表单输入的实时状态同步。border 属性的方位控制({ top: 1 }、{ topLeft: 4, topRight: 4 })实现了精细化的边框样式。position 属性的百分比定位实现了弹窗的灵活布局定位。shadow 属性的 radius/color/offsetY 三参数配置实现了立体投影效果。这些 API 24 的能力综合运用,构建了一个功能完整、交互丰富、视觉精美的企业级考勤管理应用。
更多推荐



所有评论(0)