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

实例:库存进销存(Inventory)|风格:深色数字面板

一、设计理念:为什么库存管理要用深色界面

前五个实例的页面都是浅色清新风(白底、浅灰、柔和色),实例 6 第一次采用深色数字面板风格——页面背景浅灰,核心统计区是深蓝黑渐变,数字用荧光色(绿/黄/红)呈现。为什么库存管理适合深色?

  1. 数字优先:库存管理者的核心诉求是「一眼扫过所有数字」——荧光绿(库存充足)、琥珀黄(总量)、警示红(预警)在深色背景上对比度极高,数字可读性远超浅色底;
  2. 驾驶舱隐喻:飞机驾驶舱、交易终端都是深色背景 + 高亮数字——因为深色减少视觉干扰,让注意力集中在数据本身;
  3. 警示氛围:库存预警是「紧张感」场景,深色基调天然传递「需要关注」的情绪。

页面信息架构:

  1. 标题栏:进销存标题 + 事务标语 + 刷新;
  2. 深色统计面板:商品种类 / 库存总件数 / 预警商品 三列大数字;
  3. 库存清单:每行商品 + 当前库存 + 预警状态 + 出入库按钮;
  4. 最近流水:出入库历史(方向图标 + 商品名 + 数量 + 金额);
  5. 出入库弹窗:选择类型(入库/出库)+ 数量 + 单价。

二、页面骨架:Column + Scroll

本实例没有右下角悬浮按钮(出入库操作在每行商品上),所以根节点用 Column 而非 Stack:

build() {
  Column() {
    // 标题栏
    Row() { ... }
    // 可滚动内容
    Scroll() {
      Column() {
        // 深色统计面板
        // 库存清单
        // 最近流水
      }
    }
    .width('100%').layoutWeight(1).scrollBar(BarState.Off)
  }
  .width('100%').height('100%').backgroundColor('#F1F5F9')

  // ===== 出入库弹窗 =====
  if (this.opVisible) {
    Column() { ... }
  }
}

注意弹窗 if (this.opVisible) 在 Column 内部(根节点下方)——ArkTS 要求 @Entry 组件的 build 只能有一个根节点,弹窗必须放进根容器内。这是早期版本踩过的坑(弹窗写在 build 最外层导致 build method can have only one root node 编译错误),已修正。

三、深色统计面板:荧光三数字

统计面板是页面的视觉核心,深蓝黑渐变 + 三列荧光数字:

Row() {
  Column() {
    Text(String(this.total)).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#4ADE80')
    Text('商品种类').fontSize(11).fontColor('#94A3B8').margin({ top: 2 })
  }.layoutWeight(1)
  Column() {
    Text(String(this.stockSum)).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#FBBF24')
    Text('库存总件数').fontSize(11).fontColor('#94A3B8').margin({ top: 2 })
  }.layoutWeight(1)
  Column() {
    Text(String(this.warnCount)).fontSize(28).fontWeight(FontWeight.Bold)
      .fontColor(this.warnCount > 0 ? '#F87171' : '#4ADE80')
    Text('预警商品').fontSize(11).fontColor('#94A3B8').margin({ top: 2 })
  }.layoutWeight(1)
}
.width('94%').padding({ top: 20, bottom: 20 })
.borderRadius(16)
.linearGradient({ angle: 180, colors: [['#0F172A', 0], ['#1E293B', 1]] })
.margin({ top: 10 })

色彩语义

数据 正常色 警示色 含义
商品种类 荧光绿 #4ADE80 数量本身无警示
库存总件数 琥珀黄 #FBBF24 总量中性
预警商品 荧光绿 警示红 #F87171 >0 即红

fontColor 三元切换:this.warnCount > 0 ? '#F87171' : '#4ADE80'——有预警就变红,无预警保持绿。数字颜色随状态变化是数据面板的通用设计,让异常一眼可见。

linearGradient({ angle: 180, colors: [['#0F172A', 0], ['#1E293B', 1]] }) 是上下渐变的深色面板底色。

四、库存清单:预警优先 + 出入库入口

库存清单每行包含:状态图标(⚠️/✅)、名称分类、库存数字(预警红)、出入库按钮:

ForEach(this.products, (p: Product) => {
  Row({ space: 10 }) {
    Text(p.stock <= p.warnLine ? '⚠️' : '✅').fontSize(18)
    Column({ space: 2 }) {
      Text(p.name).fontSize(15).fontWeight(FontWeight.Medium)
      Text(`${p.category} · 单价 ¥${p.price.toFixed(2)}/${p.unit}`).fontSize(11).fontColor('#999999')
    }.alignItems(HorizontalAlign.Start).layoutWeight(1)
    Column() {
      Text(`${p.stock} ${p.unit}`)
        .fontSize(16).fontWeight(FontWeight.Bold)
        .fontColor(p.stock <= p.warnLine ? '#DC2626' : '#111827')
      Text(p.stock <= p.warnLine ? `低于预警线 ${p.warnLine}` : `预警线 ${p.warnLine}`)
        .fontSize(10).fontColor('#999999').margin({ top: 2 })
    }
    Text('出入库').fontSize(12).padding({ left: 10, right: 10, top: 5, bottom: 5 })
      .borderRadius(12).backgroundColor('#3B82F6').fontColor(Color.White)
      .onClick(() => {
        this.opProductId = p.id;
        this.opQty = ''; this.opPrice = '';
        this.opVisible = true;
      })
  }
  .width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
}, (p: Product) => `${p.id}-${p.name}`)

预警双重提示:行首 ⚠️ 图标 + 库存数字红色加粗 + 副标题「低于预警线 X」——三重视觉强化预警商品,运营人员扫一眼就能锁定需要补货的商品。

出入库按钮:点击记录 opProductId(当前商品)并清空表单,打开弹窗。每次打开都重置输入,避免上次操作残留数据。

五、最近流水列表:方向 + 商品 + 金额

流水列表展示最近 15 条出入库记录:

ForEach(this.flows, (f: StockFlow) => {
  Row({ space: 10 }) {
    Text(f.type === 0 ? '📥' : '📤').fontSize(18)
    Column({ space: 2 }) {
      Text(`${f.productName} × ${f.quantity}`).fontSize(14).fontWeight(FontWeight.Medium)
      Text(this.fmtTime(f.flowTime)).fontSize(11).fontColor('#999999')
    }.alignItems(HorizontalAlign.Start).layoutWeight(1)
    Text(`${f.type === 0 ? '+' : '-'}${f.totalPrice.toFixed(2)}`)
      .fontSize(14).fontWeight(FontWeight.Bold)
      .fontColor(f.type === 0 ? '#059669' : '#DC2626')
  }
  .width('100%').padding(12).backgroundColor(Color.White).borderRadius(10).margin({ top: 8 })
}, (f: StockFlow) => `${f.id}-${f.flowTime}`)

方向语义:📥 入库(绿色 + 金额)、📤 出库(红色 - 金额)。金额是 totalPrice(联表查询已计算好),商品名来自 JOIN 填充的 productName 字段。流水时间用 fmtTime 格式化为「6月15日 14:30」。

六、出入库弹窗:类型切换 + 数量单价

点击任意商品的「出入库」弹出操作表单:

if (this.opVisible) {
  Column() {
    Text('📦 出入库操作').fontSize(18).fontWeight(FontWeight.Bold)
    Row({ space: 8 }) {
      Button('入库').layoutWeight(1).backgroundColor(this.opType === 0 ? '#059669' : '#EEF2F7')
        .fontColor(this.opType === 0 ? Color.White : '#555555')
        .onClick(() => this.opType = 0)
      Button('出库').layoutWeight(1).backgroundColor(this.opType === 1 ? '#DC2626' : '#EEF2F7')
        .fontColor(this.opType === 1 ? Color.White : '#555555')
        .onClick(() => this.opType = 1)
    }.margin({ top: 12 })
    TextInput({ placeholder: '数量 *', text: this.opQty }).type(InputType.Number)
      .margin({ top: 10 }).onChange((v: string) => this.opQty = v)
    TextInput({ placeholder: '单价(留空用进价)', text: this.opPrice }).type(InputType.Number)
      .margin({ top: 8 }).onChange((v: string) => this.opPrice = v)
    Row({ space: 8 }) {
      Button('取消').layoutWeight(1).backgroundColor('#EEF2F7').fontColor('#555555')
        .onClick(() => this.opVisible = false)
      Button('确认').layoutWeight(1).backgroundColor('#3B82F6')
        .onClick(() => this.doStockInOut())
    }.margin({ top: 16 })
  }
  .padding(20).borderRadius(16).backgroundColor(Color.White).width('88%')
  .position({ x: '6%', y: '30%' })
}

交互要点

  • 入库/出库按钮互斥高亮(opType 0/1),绿色入库红色出库;
  • 数量 InputType.Number 数字键盘;单价留空则用商品进价(doStockInOutprice > 0 ? price : product.price 兜底);
  • 确认后调用 doStockInOut(),内部走 6-3 文章将讲的事务流程。

七、刷新与状态

async refresh(): Promise<void> {
  try {
    await InventoryDao.initSeedData(this.context);
    this.products = await InventoryDao.queryProducts(this.context);
    this.flows = await InventoryDao.queryFlows(this.context, 15);
    const stats: InventoryStats = await InventoryDao.statistics(this.context);
    this.total = stats.total;
    this.stockSum = stats.stockSum;
    this.warnCount = stats.warnCount;
  } catch (e) {
    promptAction.showToast({ message: `加载失败: ${e}` });
  }
}

四个数据源一次刷新:商品列表、最近流水、三个统计数字。出入库成功后调用 refresh 全量更新——列表、流水、统计面板实时同步。

八、UI 风格要素一览

风格项 取值 说明
页面背景 #F1F5F9 浅冷灰 衬托白卡与深面板
统计面板 深蓝黑渐变 #0F172A→#1E293B 驾驶舱质感
荧光数字 绿 #4ADE80 / 黄 #FBBF24 / 红 #F87171 高对比数据
预警红 #DC2626 库存数字 缺货标红
入库绿 #059669 正向语义
出库红 #DC2626 负向语义
主色 #3B82F6 操作按钮

九、文章小结

本篇文章完成了实例 6 的 UI 层:深色数字面板(荧光三数字 + 渐变背景)+ 预警优先库存清单 + 出入库流水列表 + 操作弹窗。设计核心是「深色 + 高对比数字」让数据可读性最大化,预警三重视觉强化让缺货商品一眼锁定。所有操作(出入库)都收敛到弹窗,由 6-3 的事务逻辑保证数据一致。

下一篇(6-3)是实例 6 的技术深水区——TRANSACTION 事务如何保证「流水 + 库存」原子更新,以及库存不足的预检回滚。

十、常见问题 FAQ

Q1:为什么本实例不用 Stack 根节点(没有悬浮按钮)?
A:因为出入库操作是「每行商品的按钮」,不是全局悬浮动作——没有元素需要叠在内容之上。根节点用 Column 更简洁。根容器选择取决于是否有悬浮元素,这是 ArkUI 布局的第一个决策点。

Q2:统计面板的数字为什么是 28fp 大字号?
A:驾驶舱隐喻的核心——大数字让「扫一眼」成为可能。仓管员看板不需要小字说明,需要的是「库存 377、预警 4」这种即时可读的硬数据。字号与信息层级强相关,关键数字用大字号是数据面板铁律。

Q3:预警商品为什么排在列表顶部?
A:数据层 orderByAsc('stock')(库存升序)让库存最少的商品自然浮到顶部,无需前端二次排序。排序逻辑下沉到 SQL 是本项目的一贯原则——页面拿到即终态。

Q4:出入库弹窗每次打开为什么要清空输入?
A:避免「上次操作残留」——如果上次输了 10 件取消,下次打开还显示 10,用户容易误确认。清空输入是模态表单的卫生习惯。

Q5:流水只显示 15 条,更多历史怎么看?
A:当前 Demo 用 queryFlows(context, 15) 限制条数,简化列表。生产环境应加分页(LIMIT/OFFSET),实例 8(商品分页列表)会专门讲分页——这里的 15 条限制是「分页雏形」。

Q6:深色面板在浅色页面里会不会突兀?
A:不会——这正是「视觉锚点」设计:整页浅色,唯独统计面板深色,用户的视线自然被深色区域吸引。用对比创造焦点,深色面板是刻意的视觉中心而非不协调。

Q7:出入库按钮放行内会不会误触?
A:行内按钮是明确的「操作入口」,配合弹窗二次确认(先选类型再输入数量),误触成本低。如果担心误触,可改为「左滑显示操作」——但复杂手势在 Demo 阶段收益低,保持直接按钮。

Q8:fmtTime 的格式为什么是「6月15日 14:30」?
A:中文场景的友好格式——去掉年份(流水列表都是近期的,年份冗余),保留月日时分。时间格式是产品细节,这里展示「格式化函数」的思想:时间戳 → 可读文案的转换集中在工具方法,便于统一调整。

Logo

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

更多推荐