鸿蒙 ArkTS 实战:Reimbursement List 从业务状态到页面交互完整解析
鸿蒙 ArkTS 实战:Reimbursement List 从业务状态到页面交互完整解析
前言
报销清单器 是一个面向 企业事务管理 的鸿蒙 ArkTS 应用。记录商户、金额、票据拍照状态、报销流转和表格导出状态。 本文基于项目中的 entry/src/main/ets/pages/Index.ets 展开,拆解它的 数据接口、响应式状态、业务方法、Builder 组件 和页面布局,让读者能从源码层面理解这个小应用的实现路径。

图示说明:页面以办公场景为背景,通过输入框、筛选按钮、状态卡片和列表卡片完成核心操作闭环。
一、应用定位与业务闭环
1.1 场景定位
Reimbursement List 的价值在于把原本分散在表格、聊天记录或纸质便签里的信息,收拢到一个可操作的单页应用中。用户打开页面后可以查看统计、录入数据、切换状态,并通过列表看到最新结果。
| 业务环节 | 页面表达 | ArkTS 实现点 |
|---|---|---|
| 数据录入 | 输入框和按钮 | TextInput、Button |
| 状态展示 | 顶部统计和标签 | @State 派生计算 |
| 列表管理 | 卡片式条目 | ForEach + Builder |
| 状态流转 | 完成、推进、筛选 | private 方法 |
| 反馈输出 | 文案、颜色、消息 | 条件渲染 |
1.2 文章拆解重点
本文按照源码结构拆解:
- 先看工程入口和页面骨架。
- 再看 interface 如何约束业务数据。
- 接着分析 @State 如何驱动页面刷新。
- 最后看方法和 Builder 如何组合成完整交互。
核心观点:单页 ArkTS 应用的质量,主要取决于数据模型是否清晰、状态更新是否可追踪、UI 片段是否有明确边界。
二、工程结构与入口页面
2.1 目录位置
核心代码位于 entry/src/main/ets/pages/Index.ets。这个文件承载页面入口、数据声明、业务方法和 UI 组件。
Reimbursement List
├── AppScope
├── entry
│ └── src
│ └── main
│ ├── ets
│ │ └── pages
│ │ └── Index.ets
│ └── module.json5
├── hvigor
├── build-profile.json5
└── oh-package.json5
2.2 页面入口结构
ArkTS 页面使用 @Entry 和 @Component 标记入口组件。uild() 方法描述页面树。
@Entry
@Component
struct Index {
build() {
Scroll() {
Column() {
// header
// form
// list
}
}
.height('100%')
.width('100%')
}
}
2.3 技术构成
| 技术点 | 作用 | 在本项目中的价值 |
|---|---|---|
| ArkTS | 类型与业务逻辑 | 定义接口、方法、状态 |
| ArkUI | 声明式 UI | 构建卡片和列表 |
| @State | 响应式状态 | 驱动页面更新 |
| @Builder | UI 片段复用 | 降低重复布局 |
| Hvigor | 构建工具 | 生成 HAP 包 |
三、数据接口设计
3.1 接口清单
源码中定义的接口包括:$iface。这些接口是页面业务的基础。
interface ExpenseItem {
id: number;
title?: string;
name?: string;
status?: string;
done?: boolean;
}
实际源码会根据业务使用更具体的字段,例如客户阶段、任务优先级、报销金额、模板类别、风险等级等。接口让每条记录的结构稳定,避免列表渲染时字段不一致。
3.2 字段分层
| 字段类型 | 常见字段 | 用途 |
|---|---|---|
| 标识字段 | id | 作为列表 key 和操作目标 |
| 展示字段 | itle、 | |
| ame | 显示主标题 | |
| 分类字段 | status、 ype、category | 筛选和标签颜色 |
| 数值字段 | mount、progress、score | 统计和进度展示 |
| 布尔字段 | done、 avorite、enabled | 控制开关状态 |
3.3 为什么先建模
对于办公工具类应用,数据字段往往比页面样式更重要。先定义接口,可以让新增、过滤、推进、导出等方法都围绕同一个结构展开。
四、响应式状态拆解
4.1 状态清单
项目中的 @State 声明包括:$state。
@State records: ExpenseItem[] = [];
@State draftText: string = '';
@State filter: string = '全部';
@State message: string = '';
这些状态可以分成列表状态、输入状态、筛选状态和反馈状态。页面每次交互,最终都会落到这些状态的变化上。
4.2 状态分类表
| 状态类别 | 作用 | 示例 |
|---|---|---|
| 列表状态 | 保存业务数据 | $primaryState |
| 输入状态 | 接收用户填写内容 | draft、keyword、mountDraft |
| 筛选状态 | 控制当前视图 | ilter、selectedStatus |
| 消息状态 | 输出操作结果 | message、exportMessage |
| 游标状态 | 标记当前条目 | cursor、ctiveCustomerId |
4.3 状态更新方式
列表更新通常使用新数组替换旧数组,保持响应式刷新稳定。
private updateRecord(id: number): void {
this.records = this.records.map((item: ExpenseItem) => {
if (item.id !== id) {
return item;
}
return {
...item,
done: !item.done
};
});
}
五、核心方法解析
5.1 方法清单
源码中的核心方法包括:$method。这些方法决定了页面能做什么。
| 方法类型 | 典型职责 | 结果 |
|---|---|---|
| 统计方法 | 计算数量、金额、进度、平均值 | 顶部数据卡刷新 |
| 新增方法 | 从输入状态创建新记录 | 列表新增 |
| 流转方法 | 改变状态、阶段、进度 | 卡片文案变化 |
| 筛选方法 | 根据类别或关键词过滤 | 列表范围变化 |
| 输出方法 | 生成导出消息或预览内容 | 反馈区变化 |
5.2 新增记录
新增方法一般先读取输入状态,去除空白,再构造对象并插入列表顶部。
private addRecord(): void {
const title = this.draftText.trim();
if (title.length === 0) {
return;
}
const id = this.records.reduce((maxId: number, item: ExpenseItem) => Math.max(maxId, item.id), 0) + 1;
const record: ExpenseItem = {
id: id,
title: title,
done: false
};
this.records = [record, ...this.records];
this.draftText = '';
}
5.3 状态推进
状态推进类方法通常使用 map 保持不可变更新风格。
private nextStatus(id: number): void {
this.records = this.records.map((item: ExpenseItem) => {
if (item.id !== id) {
return item;
}
return {
...item,
status: '下一阶段'
};
});
}
5.4 筛选列表
筛选方法把状态与列表结合,形成当前可见数据。
private visibleRecords(): ExpenseItem[] {
return this.records.filter((item: ExpenseItem) => {
return this.filter === '全部' || item.status === this.filter;
});
}
六、Builder 组件设计
6.1 Builder 清单
源码中的 Builder 组件包括:$builder。Builder 用于封装重复 UI,例如标签页、卡片、列表行、栏目区域。
@Builder
private card(item: ExpenseItem) {
Column() {
Text(item.title ?? item.name ?? '未命名')
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
6.2 复用边界
| UI 片段 | 适合放入 Builder 的原因 |
|---|---|
| 状态标签 | 选中态、颜色、点击动作重复 |
| 列表卡片 | 每条记录结构一致 |
| 统计卡 | 数值和标题样式一致 |
| 输入区块 | 多字段录入布局稳定 |
6.3 Builder 与方法分工
Builder 负责渲染,private 方法负责业务变化。这样代码阅读时可以快速区分“页面长什么样”和“点击后发生什么”。
七、页面布局结构
7.1 纵向滚动布局
办公工具通常信息较多,采用 Scroll + Column 更稳妥。
Scroll() {
Column() {
this.headerView()
this.formView()
ForEach(this.visibleRecords(), (item: ExpenseItem) => {
this.card(item)
}, (item: ExpenseItem) => item.id.toString())
}
.padding(16)
}
.backgroundColor('#F5F7FA')
7.2 页面分区
| 区域 | 功能 | 用户关注点 |
|---|---|---|
| 顶部概览 | 标题、说明、统计 | 当前整体状态 |
| 输入区域 | 新增或编辑信息 | 快速录入 |
| 筛选区域 | 状态、类别、关键词 | 缩小范围 |
| 列表区域 | 业务条目 | 执行操作 |
| 反馈区域 | 导出、提示、预览 | 操作结果 |
7.3 列表 key
使用 id.toString() 作为 key,能让列表渲染更稳定。
ForEach(this.records, (item: ExpenseItem) => {
this.card(item)
}, (item: ExpenseItem) => item.id.toString())
八、交互链路分析
8.1 输入到新增
输入框通过 onChange 写入草稿状态,按钮点击后调用新增方法。
TextInput({ text: this.draftText })
.onChange((value: string) => {
this.draftText = value;
})
Button('添加')
.onClick(() => {
this.addRecord();
})
8.2 卡片到状态流转
卡片按钮把 id 传回方法,方法再更新列表。
Button('推进')
.onClick(() => {
this.nextStatus(item.id);
})
8.3 统计同步
顶部统计不单独保存结果,而是从列表派生。
private activeCount(): number {
return this.records.filter((item: ExpenseItem) => !item.done).length;
}
关键点:统计值从源数据计算,而不是额外维护一份副本,可以减少状态不同步的问题。
九、办公场景适配细节
9.1 信息密度
Reimbursement List 属于办公效率工具,页面应优先保证信息清楚,而不是追求复杂视觉效果。卡片中主标题、状态、辅助字段和操作按钮要保持层级明确。
9.2 颜色语义
| 颜色语义 | 适用状态 |
|---|---|
| 绿色 | 已完成、通过、安全 |
| 橙色 | 待处理、临近、关注 |
| 红色 | 风险、逾期、异常 |
| 蓝色 | 当前选中、主操作 |
| 灰色 | 辅助文本、禁用状态 |
9.3 文案原则
按钮文案应直接描述动作,例如添加、推进、导出、复制、预约、完成。状态文案应描述当前结果,例如待处理、已完成、进行中、已导出。
十、运行与调试
10.1 DevEco Studio 运行
# 打开项目目录
# 同步 oh-package 依赖
# 选择 entry 模块
# 连接模拟器或真机运行
10.2 Hvigor 构建
hvigorw clean
hvigorw assembleHap
10.3 调试重点
| 调试点 | 判断方式 |
|---|---|
| 输入是否生效 | 输入后状态字段是否变化 |
| 列表是否刷新 | 新增或推进后卡片是否更新 |
| 筛选是否正确 | 切换标签后数量是否变化 |
| 统计是否同步 | 顶部数字是否来自当前列表 |
| 按钮是否可达 | 点击区域是否足够明确 |
十一、代码可维护性
11.1 文件规模
当前 Index.ets 约 325 行,适合单页示例。后续如果继续扩展,可以把卡片、表单和统计区拆成独立组件。
11.2 命名一致性
$primaryMethod 等方法名都表达具体业务动作。方法名越贴近业务,页面代码越容易维护。
11.3 状态收敛
不要为每个展示值都创建 @State。能从列表计算出来的值,优先使用方法派生。
private summary(): number {
return this.records.length;
}
十二、可扩展方向
12.1 本地持久化
可以把核心列表序列化到本地存储,进入页面时恢复。
interface StoredRecord {
id: number;
payload: string;
updatedAt: number;
}
12.2 搜索与排序
列表较长后,可以增加关键词搜索、状态排序和时间排序。
12.3 数据导出
办公类应用常见需求是导出表格、复制模板、生成二维码或分享文本。当前项目中的消息状态和导出方法已经具备扩展基础。
十三、同类应用复用模板
13.1 最小结构
interface ItemModel {
id: number;
title: string;
status: string;
}
@State items: ItemModel[] = [];
@State draftTitle: string = '';
@State filter: string = '全部';
13.2 最小交互
private addItem(): void {
const title = this.draftTitle.trim();
if (title.length === 0) {
return;
}
this.items = [{ id: Date.now(), title: title, status: '待处理' }, ...this.items];
this.draftTitle = '';
}
13.3 最小展示
@Builder
private itemRow(item: ItemModel) {
Row() {
Text(item.title).layoutWeight(1)
Text(item.status)
}
.padding(12)
.backgroundColor('#FFFFFF')
}
十四、项目实现亮点
14.1 业务对象清晰
Reimbursement List 使用 $iface 管理业务数据,接口字段和页面操作对应关系明确。
14.2 状态变化集中
$state 覆盖了页面主要交互状态,状态入口集中在 Index 组件内,便于定位问题。
14.3 组件复用明确
$builder 让列表和筛选区域保持一致样式,减少重复代码。
十五、总结
Reimbursement List 展示了鸿蒙 ArkTS 办公类单页应用的典型实现方式:先用接口描述业务记录,再用 @State 保存列表、输入和筛选状态,通过 $method 完成新增、筛选、推进和统计,最后用 $builder 把数据渲染为稳定的卡片与操作区。
这类项目的关键不在于代码量,而在于状态链路是否完整。只要保证输入、状态、方法和 UI 四个环节一致,后续扩展本地存储、导出、搜索、通知或多页面跳转都可以建立在现有结构之上。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐




所有评论(0)