在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

鸿蒙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 组件接收 itemindexonChange 回调,将每一行封装成独立的可复用单元——这与 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);
          }
        })
      // ... 差异列 ...
    }
  }
}

设计要点

  1. 局部状态@State inputText 管理 TextInput 的当前值,确保输入过程中 UI 流畅不卡顿
  2. 回调委托onChange 回调将值传递给父组件,由 InventoryPage 统一更新数据模型
  3. 数字键盘InputType.Number 在移动端弹出数字键盘,提升输入效率
  4. 初始化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 项),建议:

  1. 使用 LazyForEach:替代 ForEach,支持按需渲染(虚拟列表)
  2. 减少列数:移动端建议不超过 6 列
  3. 固定行高:使用固定 height 避免动态计算
  4. 节流输入:TextInput 的 onChange 回调可以进行节流(debounce),减少高频更新带来的渲染压力

6.2 数据持久化

当前实现使用内存数据,未做持久化。真实项目中建议:

用户编辑 → 本地缓存(SQLite / 文件) → 批量提交 → 后端 API
                          ↓
                    应用重启时恢复

鸿蒙提供了 data.preferences(首选项)和 relationalStore(关系型数据库)两种本地存储方案,可以根据数据量选择。

6.3 网络交互

批量保存时,如果数据量大,建议:

  1. 分片提交:将 100 条数据分为 10 片,每片 10 条并发提交
  2. 离线缓存:在网络不可用时暂存本地,网络恢复后自动同步
  3. 加载状态:保存过程中显示 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 功能扩展

当前实现的盘点表可以进行以下扩展:

  1. 条码扫描:集成 @kit.ScanKit 实现扫码录入商品编码
  2. 拍照取证:差异项一键拍照留存证据
  3. 多仓库支持:添加仓库选择器
  4. 盘点单管理:历史盘点记录查询、对比
  5. Excel 导入/导出:与现有系统数据互通
  6. 审批流程:差异超过阈值时触发审批

8.2 适配多种设备

ArkUI 的 @Styles@Extend 可以方便地实现多设备适配:

// 平板模式使用更宽的布局
if (this.isTablet) {
  // 使用 12 列表格,显示更多信息
} else {
  // 手机模式使用 6 列精简布局
}

8.3 与后端系统对接

一个完整的进销存盘点流程需要与后端系统配合:

前端盘点 → API 提交 → 后端校验
    ↓                      ↓
生成盘点差异单 ← 更新库存 ← 审核通过
    ↓
财务调账 → 成本重算

九、总结

本文详细介绍了如何使用 鸿蒙 ArkUI(ArkTS) 框架实现一个 库存盘点表 应用模块。从技术选型、架构设计到编码实现,全面覆盖了以下核心主题:

  1. 表格布局方案:使用 Column + Row 组合模拟 DataTable,以百分比列宽实现响应式适配
  2. 内联编辑机制:TextInput 嵌入表格行,通过单向数据流和回调实现批量编辑
  3. 实时计算与视觉反馈:盘点数量变更时自动计算差异,并结合语义化颜色进行视觉提示
  4. 批量保存流程:一键汇总所有盘点数据,弹窗确认后提交
  5. 鸿蒙最佳实践:状态管理、组件通信、ArkTS 语法限制、资源引用等

该盘点表模块定位为 进销存管理系统 的组成部分,以轻量、直观、高效为设计目标,适用于门店、仓库、工厂等场景的移动盘点需求。

通过这个实践项目可以看出,鸿蒙 ArkUI 在 企业级数据录入场景 中具有出色的表达能力。其声明式 UI 模型让开发者能够用较少的代码构建出交互流畅、可维护性高的数据管理界面。结合鸿蒙生态的分布式能力、设备协同和系统级安全机制,ArkUI 有望成为企业移动信息化建设的有力技术选择。

主要参考


本文档对应项目:MyApplication74 (HarmonyOS 6.1.1, API 24)
创建日期:2026-07-05

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐