鸿蒙ArkUI 库存盘点表设计与实现 — 基于DataTable+TextInput的进销存应用实践



鸿蒙ArkUI 库存盘点表设计与实现 — 基于DataTable+TextInput的进销存应用实践
一、引言
1.1 背景
在企业进销存(采购—入库—销售—库存)管理系统中,库存盘点 是一项至关重要的基础业务。所谓盘点,是指企业定期或不定期地对仓库中的实物库存进行清点、核对,将实际库存数量与账面记录进行对比,从而发现差异、分析原因并调整账目。盘点的准确性直接影响企业的成本核算、供应链效率和财务数据可信度。
传统的库存盘点通常依赖纸质单据或Excel表格,仓库人员手工记录盘点数量,再由文员逐一录入系统。这种方式效率低下、容易出错,且无法实时反映盘点进度。随着企业数字化转型的深入,越来越多的企业希望在移动设备上完成盘点工作——仓库人员手持终端,逐项扫描商品、录入实盘数量,系统即时计算差异并批量提交。
本文基于 HarmonyOS(鸿蒙)ArkUI 框架,详细介绍如何利用 DataTable(表格布局)+ TextInput(文本输入)组件,构建一个 库存盘点表 应用模块。该模块以 DataTable 风格的 UI 呈现库存列表,每行提供 TextInput 让用户编辑盘点数量,并通过「保存盘点」按钮实现批量更新。文章将从技术选型、架构设计、代码实现到性能优化,全面剖析这一典型进销存场景的鸿蒙原生开发实践。
1.2 适用场景
- 仓库移动盘点:仓库管理员手持鸿蒙设备(平板、手机),逐项录入实盘数量
- 门店零售盘点:门店员工对店内商品进行周期性盘点
- 生产物料盘点:工厂车间对原材料、半成品进行盘点
- 资产盘点:企业对固定资产进行清查登记
1.3 技术栈
| 技术 | 版本 | 说明 |
|---|---|---|
| HarmonyOS | 6.1.1 (API 24) | 操作系统及 SDK |
| ArkUI (ArkTS) | 6.x | 声明式 UI 框架 |
| Hvigor | 6.24.2 | 构建工具 |
| TypeScript | — | ArkTS 基于 TypeScript 语法扩展 |
二、项目架构与设计思路
2.1 总体架构
整个库存盘点模块采用 分层解耦 的设计思想,分为以下三层:
┌──────────────────────────────────────────┐
│ UI 表现层(Pages) │
│ Index.ets(首页导航) │
│ InventoryPage.ets(盘点表页面) │
├──────────────────────────────────────────┤
│ 组件层(Components) │
│ InventoryRow(单行数据组件) │
├──────────────────────────────────────────┤
│ 数据模型层(Interfaces) │
│ InventoryItem(盘点数据接口) │
└──────────────────────────────────────────┘
- 表现层:负责页面布局、用户交互和导航跳转
- 组件层:封装可复用的 UI 组件,如表中的每一行
- 数据模型层:定义数据结构接口,确保类型安全
2.2 为什么选择 DataTable + TextInput 方案
在移动端表格编辑场景中,常见的方案有三种:
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| DataTable + TextInput | 直观展示全部数据,可批量编辑 | 数据量大时滚动性能下降 | 50条以内的小规模盘点 |
| 列表 + 详情页编辑 | 单条操作流畅 | 无法总览全局,切换繁琐 | 数据量大、字段多 |
| 扫描枪 + 自动填充 | 高效率、低错误 | 需要硬件支持 | 专业仓库盘点 |
我们的盘点表场景具有以下特点:
- 盘点商品通常在 10~50 项(门店/小仓库级别)
- 需要 总览全局,一眼看出哪些商品有差异
- 操作人员需要在 同一屏内 快速修改多个数量
因此,DataTable + TextInput 方案是最优选择。表格的「差异」列帮助操作人员快速定位问题项,而 TextInput 直接内嵌在表格中,避免了「点开详情→编辑→返回」的繁琐操作链。
2.3 与 Flutter 布局理念的对比
鸿蒙 ArkUI 的布局理念与 Flutter 有诸多相似之处,这也是许多开发者称之为「鸿蒙版 Flutter」的原因:
| 维度 | Flutter | HarmonyOS ArkUI |
|---|---|---|
| 语言 | Dart | ArkTS(TypeScript 超集) |
| UI 描述 | 声明式 Widget | 声明式 @Component + @Builder |
| 组件树 | Widget 嵌套 | Column/Row/Stack 嵌套 |
| 状态管理 | StatefulWidget | @State 装饰器 |
| 响应式更新 | setState | 状态变量自动触发刷新 |
在我们的实现中,可以清晰看到这种 声明式 + 组件化 理念的体现:InventoryRow 组件接收 item、index 和 onChange 回调,将每一行封装成独立的可复用单元——这与 Flutter 的 StatelessWidget 概念如出一辙。
三、关键技术实现详解
3.1 数据模型设计
清晰的类型定义是可靠应用的基础。我们定义了一个 InventoryItem 接口:
interface InventoryItem {
id: number; // 序号(主键)
code: string; // 商品编码(SKU)
name: string; // 商品名称
bookQty: number; // 账面库存(系统记录)
countQty: number; // 盘点数量(用户填写,可编辑)
diffQty: number; // 差异数量(自动计算 = countQty - bookQty)
}
设计要点:
diffQty是计算字段,由countQty - bookQty自动得出,不在用户层面编辑id作为每行的唯一标识,用于高效地查找和更新特定行code(商品编码)用于与后端 ERP 系统对接时的主键匹配
3.2 Mock 数据设计
为演示和开发调试,我们内置了 10 条模拟数据,涵盖四种典型盘点情况:
// 盘亏 -5 件(账面100,实盘95)
{ id: 1, code: 'P001', name: 'ThinkPad X1 笔记本', bookQty: 100, countQty: 95, diffQty: -5 }
// 盘盈 +5 件(账面200,实盘205)
{ id: 2, code: 'P002', name: 'Logitech MX Master 鼠标', bookQty: 200, countQty: 205, diffQty: 5 }
// 零差异(账面50,实盘50)
{ id: 3, code: 'P003', name: 'Samsung 27寸 4K显示器', bookQty: 50, countQty: 50, diffQty: 0 }
// 略亏 -2 件(账面90,实盘88)
{ id: 10, code: 'P010', name: '索尼 WH-1000XM5', bookQty: 90, countQty: 88, diffQty: -2 }
这种数据设计覆盖了 盘盈、盘亏、零差异 三种状态,便于在开发过程中测试 UI 的差异着色逻辑和统计汇总功能。
3.3 表格布局的 ArkUI 实现
鸿蒙 ArkUI 没有内置的 DataTable 组件,因此我们使用 Column + Row 组合来模拟表格:
表头(Header)
表头使用固定 Row 布局,每个 Text 组件代表一列,通过 width 百分比控制列宽:
Row() {
Text('序号').width('8%')
Text('商品编码').width('18%')
Text('商品名称').width('30%')
Text('账面库存').width('14%').textAlign(TextAlign.End)
Text('盘点数量').width('15%').textAlign(TextAlign.End)
Text('差异').width('15%').textAlign(TextAlign.End)
}
.backgroundColor('#EDF2F7')
.borderRadius({ topLeft: 8, topRight: 8 })
- 表头固定在页面顶部,不随数据滚动
- 数量相关列采用
TextAlign.End右对齐(符合财务表格习惯) - 「盘点数量」列用蓝色 (#3182CE) 突出显示,引导用户点击编辑
数据行(Data Row)
每一行数据使用 InventoryRow 组件渲染:
ForEach(this.inventoryList, (item: InventoryItem, index?: number) => {
InventoryRow({
item: item,
index: index as number,
onChange: (id: number, value: string) => this.onCountQtyChange(id, value)
})
}, (item: InventoryItem, index?: number) => `${item.id}`)
关键设计:
ForEach的第三个参数是keyGenerator,使用item.id作为唯一标识,确保列表 diff 更新时能精确追踪每行- 每一行的背景色由
index % 2决定(隔行变色),提升长列表的可读性 - 数据区域包裹在
Scroll中,支持垂直滚动
3.4 TextInput 内联编辑的实现
这是盘点功能的核心交互:每行的「盘点数量」列是一个可编辑的 TextInput。
组件封装
@Component
struct InventoryRow {
item: InventoryItem;
index: number;
onChange?: (id: number, value: string) => void;
@State inputText: string = '0';
aboutToAppear(): void {
this.inputText = `${this.item.countQty}`;
}
build() {
Row() {
// ... 其他列 ...
TextInput({ text: this.inputText, placeholder: '输入数量' })
.type(InputType.Number)
.onChange((value: string) => {
this.inputText = value;
if (this.onChange) {
this.onChange(this.item.id, value);
}
})
// ... 差异列 ...
}
}
}
设计要点:
- 局部状态:
@State inputText管理 TextInput 的当前值,确保输入过程中 UI 流畅不卡顿 - 回调委托:
onChange回调将值传递给父组件,由InventoryPage统一更新数据模型 - 数字键盘:
InputType.Number在移动端弹出数字键盘,提升输入效率 - 初始化:
aboutToAppear生命周期中将 TextInput 初始值设为盘点数量
数据流示意
用户输入 → TextInput.onChange
↓
InventoryRow.onChange(回调)
↓
InventoryPage.onCountQtyChange(更新数组)
↓
@State inventoryList 变更 → UI 重新渲染 → 差异列更新 + 汇总数更新
↓
底部状态栏同步刷新
这种 单向数据流 的设计保证了状态的可预测性:数据从父组件流向子组件(通过属性传递),子组件通过回调将变更通知父组件,父组件修改状态后驱动 UI 刷新。
3.5 差异计算与着色逻辑
自动计算
每当用户修改盘点数量,系统立即计算差异:
onCountQtyChange(id: number, value: string): void {
const qty = parseInt(value, 10);
if (isNaN(qty)) return; // 非数字输入忽略
const index = this.inventoryList.findIndex(item => item.id === id);
if (index === -1) return;
const entry = this.inventoryList[index];
const updateList = [...this.inventoryList];
updateList[index] = {
id: entry.id,
code: entry.code,
name: entry.name,
bookQty: entry.bookQty,
countQty: qty,
diffQty: qty - entry.bookQty // 差异 = 实盘 - 账面
};
this.inventoryList = updateList; // 触发 UI 刷新
this.recalcTotalDiff(); // 重新汇总
}
视觉着色
差异值的颜色规则符合直觉:
| 差异值 | 业务含义 | 颜色 | 背景色 |
|---|---|---|---|
| > 0 | 盘盈(实盘 > 账面) | 红色 #E53E3E | 浅红 #FFF5F5 |
| < 0 | 盘亏(实盘 < 账面) | 绿色 #38A169 | 浅绿 #F0FFF4 |
| = 0 | 一致 | 灰色 #718096 | — |
颜色方案遵循 语义化设计原则:
- 红色 → 警惕/异常:盘盈意味着有多出来的商品,可能是入库未记账
- 绿色 → 正常/可接受:盘亏意味着有商品丢失或损耗
- 灰色 → 中性:完全一致,无需关注
3.6 批量保存与汇总
保存弹窗
点击「💾 保存盘点」按钮,触发 saveInventory() 方法,生成结构化的盘点明细并弹出确认对话框:
saveInventory(): void {
let msg = '盘点数据保存成功!\n\n';
msg += '盘点明细:\n';
msg += '──────────────────────────\n';
msg += '序号 | 商品名称 | 账面 | 盘点 | 差异\n';
msg += '──────────────────────────\n';
for (const item of this.inventoryList) {
const diffStr = item.diffQty > 0 ? `+${item.diffQty}` : `${item.diffQty}`;
msg += `${item.id} | ${item.name.padEnd(14)} | ` +
`${String(item.bookQty).padStart(4)} | ` +
`${String(item.countQty).padStart(4)} | ${diffStr}\n`;
}
msg += '──────────────────────────\n';
msg += `差异总数: ${this.totalDiff > 0 ? '+' : ''}${this.totalDiff}`;
AlertDialog.show({
title: '💾 保存盘点数据',
message: msg,
confirm: { value: '确定', action: () => { /* 提交到后端 */ } }
});
}
在真实企业应用中,confirm.action 回调中应调用后端 API 提交数据,例如:
// 伪代码:提交到后端
HTTP.post('/api/inventory/batch-save', {
inventoryData: this.inventoryList,
warehouseId: 'WH001',
checker: currentUser,
checkTime: new Date().toISOString()
})
汇总统计
底部状态栏实时显示四项统计指标:
Row() {
Text(`共 ${this.inventoryList.length} 项商品`)
Text(`盘盈: ${this.countBySign(true)} 项`)
Text(`盘亏: ${this.countBySign(false)} 项`)
Text(`无误: ${this.inventoryList.filter(i => i.diffQty === 0).length} 项`)
}
- 共X项:盘点商品总数
- 盘盈X项:实盘数量大于账面数量的商品数
- 盘亏X项:实盘数量小于账面数量的商品数
- 无误X项:数量一致的商品数
顶部的「差异总数」标签则反映所有商品差异的代数和,用正负号和颜色直观提示整体盘盈还是盘亏。
3.7 重置功能
如果在盘点过程中发现数据混乱,用户可以一键重置:
resetToBookQty(): void {
const resetList: InventoryItem[] = [];
for (const item of this.inventoryList) {
resetList.push({
...item,
countQty: item.bookQty, // 实盘数量 = 账面数量
diffQty: 0 // 差异清零
});
}
this.inventoryList = resetList;
this.recalcTotalDiff();
}
重置前有二次确认弹窗,防止误操作。
四、主题样式与视觉设计
4.1 设计规范
本应用的 UI 风格参考了当前主流的 企业级后台设计规范(如 Ant Design、Element UI),采用温和的中性色调,注重视觉层次和可读性:
- 主色:#3182CE(蓝色)— 用于可交互元素(按钮、可编辑列)
- 背景色:#F7FAFC(浅灰)— 页面整体背景
- 卡片色:#FFFFFF(白色)— 内容区域
- 分割线:#E2E8F0(淡灰)— 用于分隔视觉区域
- 文本色:#1A202C(深灰)— 主要文字
- 辅助文本:#A0AEC0(中灰)— 次要说明文字
4.2 隔行变色
表格数据区的每一行通过 index % 2 控制背景色:
偶数行: #FFFFFF(纯白)
奇数行: #FAFBFC(极浅灰)
这种设计在保证表格可读性的同时,避免了高对比条纹带来的视觉疲劳。
4.3 响应式适配
虽然当前页面针对手机竖屏设计,但列宽使用百分比而非固定像素,使其在不同屏幕尺寸下具有良好的自适应能力:
| 列 | 宽度占比 | 说明 |
|---|---|---|
| 序号 | 8% | 简洁的数字指示 |
| 商品编码 | 18% | SKU 标识 |
| 商品名称 | 30% | 长文本自动裁切 |
| 账面库存 | 14% | 右对齐 |
| 盘点数量 | 15% | 右对齐,可编辑 |
| 差异 | 15% | 右对齐,语义着色 |
4.4 阴影与圆角
- 标题栏使用
shadow属性添加底部阴影(2px 偏移),营造层次感 - 表头上角使用
borderRadius({ topLeft: 8, topRight: 8 })进行圆角处理 - 输入框使用 4px 圆角边框,柔化视觉
五、鸿蒙 ArkTS 开发注意要点
5.1 ArkTS 语法限制
在与 TypeScript 的兼容性上,ArkTS 有一些关键限制,我们在开发中遇到了以下几点:
| 限制 | 说明 | 解决方式 |
|---|---|---|
禁止对象展开运算符 ... |
ArkTS 不支持在对象字面量中使用 {...obj, prop: val} |
显式列出所有属性 |
| 禁止未类型的对象字面量 | 表达式中的对象字面量必须匹配显式声明的类型 | 明确标注类型 InventoryItem[] |
| 私有属性不能通过构造函数初始化 | private 属性的组件在父组件中无法通过构造函数传参 |
移除 private 修饰符 |
| 数组展开允许,对象展开禁止 | [...arr] 合法,{...obj} 不合法 |
数组复制用展开,对象复制用显式构造 |
5.2 状态管理注意事项
ArkUI 的 @State 机制基于 引用比较 判断是否需刷新。对于数组类型:
// 以下方式不会触发 UI 刷新 ❌
this.inventoryList[index].countQty = newQty;
this.inventoryList[index].diffQty = newDiff;
// 以下方式会触发 UI 刷新 ✅
this.inventoryList = [...this.inventoryList];
// 或
this.inventoryList = updateList;
这要求我们 每次修改数组时都创建新数组引用,这是声明式框架的典型约束,与 React 的 setState 和 Flutter 的 setState 原理一致。
5.3 组件通信方式
ArkUI 中的组件通信采用 单向数据流 模式:
- 父→子:
@Prop或构造函数传参 - 子→父:回调函数(
onXxx模式) - 兄弟组件:通过共同父组件中转
- 跨页面:路由传参或 AppStorage 全局状态
我们的 InventoryRow 组件使用的是构造函数传参(父→子)+ onChange 回调(子→父)的组合方式。
5.4 资源引用
鸿蒙应用中的资源引用使用 $r() 函数:
// 引用应用级媒体资源
Image($r('app.media.layered_image'))
// 引用模块级字符串资源
Text($r('app.string.module_desc'))
注意资源 ID 的命名规范:$r('app.media.xxx') 引用 AppScope 下的资源,$r('media.xxx') 引用当前 module 下的资源。
六、性能优化建议
6.1 列表渲染优化
对于大批量盘点数据(超过 100 项),建议:
- 使用 LazyForEach:替代
ForEach,支持按需渲染(虚拟列表) - 减少列数:移动端建议不超过 6 列
- 固定行高:使用固定
height避免动态计算 - 节流输入:TextInput 的
onChange回调可以进行节流(debounce),减少高频更新带来的渲染压力
6.2 数据持久化
当前实现使用内存数据,未做持久化。真实项目中建议:
用户编辑 → 本地缓存(SQLite / 文件) → 批量提交 → 后端 API
↓
应用重启时恢复
鸿蒙提供了 data.preferences(首选项)和 relationalStore(关系型数据库)两种本地存储方案,可以根据数据量选择。
6.3 网络交互
批量保存时,如果数据量大,建议:
- 分片提交:将 100 条数据分为 10 片,每片 10 条并发提交
- 离线缓存:在网络不可用时暂存本地,网络恢复后自动同步
- 加载状态:保存过程中显示 Loading 动画,防止重复点击
七、与传统开发方式的对比
7.1 对比 HTML5 方案
| 维度 | HTML5 App | HarmonyOS ArkUI |
|---|---|---|
| 开发语言 | HTML + CSS + JavaScript | ArkTS (TypeScript) |
| 渲染性能 | WebView 渲染 | 原生渲染 |
| 组件丰富度 | 依赖第三方 UI 库 | 系统级组件,深度优化 |
| 动画流畅度 | 一般 | 60fps 原生动画 |
| 设备能力调用 | 通过 Bridge/插件 | 直接调用系统 API |
7.2 对比 Flutter 方案
| 维度 | Flutter | HarmonyOS ArkUI |
|---|---|---|
| 运行时 | Flutter Engine | 鸿蒙原生运行时 |
| 开发语言 | Dart | ArkTS |
| 包体积 | 较大(含 Engine) | 较小(系统级) |
| 跨平台 | iOS/Android/Web | 仅鸿蒙 |
对于 纯鸿蒙生态 的企业级应用,使用 ArkUI 可以实现更小的包体积和更好的系统集成体验。
八、扩展与未来展望
8.1 功能扩展
当前实现的盘点表可以进行以下扩展:
- 条码扫描:集成
@kit.ScanKit实现扫码录入商品编码 - 拍照取证:差异项一键拍照留存证据
- 多仓库支持:添加仓库选择器
- 盘点单管理:历史盘点记录查询、对比
- Excel 导入/导出:与现有系统数据互通
- 审批流程:差异超过阈值时触发审批
8.2 适配多种设备
ArkUI 的 @Styles 和 @Extend 可以方便地实现多设备适配:
// 平板模式使用更宽的布局
if (this.isTablet) {
// 使用 12 列表格,显示更多信息
} else {
// 手机模式使用 6 列精简布局
}
8.3 与后端系统对接
一个完整的进销存盘点流程需要与后端系统配合:
前端盘点 → API 提交 → 后端校验
↓ ↓
生成盘点差异单 ← 更新库存 ← 审核通过
↓
财务调账 → 成本重算
九、总结
本文详细介绍了如何使用 鸿蒙 ArkUI(ArkTS) 框架实现一个 库存盘点表 应用模块。从技术选型、架构设计到编码实现,全面覆盖了以下核心主题:
- 表格布局方案:使用 Column + Row 组合模拟 DataTable,以百分比列宽实现响应式适配
- 内联编辑机制:TextInput 嵌入表格行,通过单向数据流和回调实现批量编辑
- 实时计算与视觉反馈:盘点数量变更时自动计算差异,并结合语义化颜色进行视觉提示
- 批量保存流程:一键汇总所有盘点数据,弹窗确认后提交
- 鸿蒙最佳实践:状态管理、组件通信、ArkTS 语法限制、资源引用等
该盘点表模块定位为 进销存管理系统 的组成部分,以轻量、直观、高效为设计目标,适用于门店、仓库、工厂等场景的移动盘点需求。
通过这个实践项目可以看出,鸿蒙 ArkUI 在 企业级数据录入场景 中具有出色的表达能力。其声明式 UI 模型让开发者能够用较少的代码构建出交互流畅、可维护性高的数据管理界面。结合鸿蒙生态的分布式能力、设备协同和系统级安全机制,ArkUI 有望成为企业移动信息化建设的有力技术选择。
主要参考
- HarmonyOS 开发者文档 — ArkUI 组件参考
- ArkTS 编程语言指南
- 鸿蒙 Hvigor 构建工具使用指南
本文档对应项目:MyApplication74 (HarmonyOS 6.1.1, API 24)
创建日期:2026-07-05
更多推荐




所有评论(0)