牛奶不能减成负数:冰箱库存的筛选、去重与不可变更新

库存页需要同时回答三个问题:冰箱里有什么、哪些快到期、哪些快吃完。把三类结果分别保存成数组,会让新增和数量修改变得难以同步;这里保留一份 foods,筛选结果按当前条件计算。

typescript 复制代码
interface FoodItem {
  id: number
  name: string
  category: string
  count: number
  daysLeft: number
}

@State filter: string = '全部'
@State foods: FoodItem[] = [/* 当前库存 */]
@State newName: string = ''
@State message: string = '先把今天买的东西记下来'

"临期"以剩余天数小于等于2为条件,"快吃完"以数量小于等于1为条件。两个条件都不改变原数组,只返回当前视图需要的数据。

typescript 复制代码
private visible(): FoodItem[] {
  if (this.filter === '临期') {
    return this.foods.filter((item: FoodItem) => item.daysLeft <= 2)
  }
  if (this.filter === '快吃完') {
    return this.foods.filter((item: FoodItem) => item.count <= 1)
  }
  return this.foods
}

新增前先处理空名称和重复名称。重复判断使用 some(),命中后不改数组;成功记录放在顶部,并清空输入框。

typescript 复制代码
private addFood(): void {
  const name: string = this.newName.trim()
  if (name.length === 0) {
    this.message = '食材名称不能为空'
    return
  }
  if (this.foods.some((item: FoodItem) => item.name === name)) {
    this.message = `${name} 已经在冰箱里了`
    return
  }

  const item: FoodItem = {
    id: Date.now(), name, category: '其他', count: 1, daysLeft: 5
  }
  this.foods = [item, ...this.foods]
  this.newName = ''
  this.message = `已添加 ${name}`
}

数量调整通过 map() 替换目标对象。减号连续点击时,用 Math.max(0, ...) 把下限锁在0。若允许负库存,"快吃完"筛选仍会显示它,但数值已经失去业务含义。

typescript 复制代码
private changeCount(id: number, delta: number): void {
  this.foods = this.foods.map((item: FoodItem) =>
    item.id === id
      ? { ...item, count: Math.max(0, item.count + delta) }
      : item
  )
}

列表渲染时,临期状态同时影响说明文字和卡片背景;数量按钮只把ID与增量交给 changeCount()。视图层不直接修改对象。

typescript 复制代码
Text(`${item.category} · ${
  item.daysLeft <= 2 ? `还剩${item.daysLeft}天` : '状态正常'
}`)
  .fontColor(item.daysLeft <= 2 ? '#D45D4C' : '#778296')

Button('−')
  .onClick(() => {
    this.changeCount(item.id, -1)
  })

Button('+')
  .onClick(() => {
    this.changeCount(item.id, 1)
  })

提示颜色也来自消息内容。空名称和重复名称使用错误色,正常新增保持中性色。这种字符串判断适合当前少量文案,但继续增加错误类型时应改成结构化状态,例如单独保存 messageType,避免改一句提示就影响颜色逻辑。

初始页面把临期数量放在标题下方,临期卡片使用不同背景和文字色。这个数量直接从 foods 过滤,不额外维护计数状态。

输入已存在的"牛奶"时,页面停在重复提示,列表保持原样;改成新名称后才会插入记录。切换临期筛选后,只显示符合剩余天数条件的食材。

当前 daysLeft 是初始化时写入的相对天数,还没有关联真实日期;数量单位也统一为件。完善库存模型时,应保存到期日期和单位,再根据当天日期派生剩余天数。现有筛选与更新结构可以继续使用,只需要替换派生条件。

数量减到0以后当前记录不会自动删除,这是刻意保留的边界:0可以表示"已经吃完,等待补货"。如果直接过滤掉0库存,误点减号后就无法恢复。后续可以增加归档或删除动作,把数量变化和记录生命周期分开处理。

顶部数量来自同一份库存

标题下方显示食材总种类与临期种类,两项都从 foods 计算。新增会改变总数,到期条件变化会改变临期数,不需要额外维护计数器。

typescript 复制代码
Text(`${this.foods.length} 种食材 · ${
  this.foods.filter(
    (item: FoodItem) => item.daysLeft <= 2
  ).length
} 种临期`)

当前 daysLeft 不可编辑,因此临期数量只随新增数据变化。接入真实日期后,应保存到期时间,由当天日期派生剩余天数;每天打开应用都能得到新结果,无需每天批量改写全部记录。

三个筛选按钮共享可见函数

按钮只更新 filterList 始终遍历 visible()。选中样式也从同一字符串判断。

typescript 复制代码
ForEach(['全部', '临期', '快吃完'], (name: string) => {
  Button(name)
    .fontColor(this.filter === name ? Color.White : '#596579')
    .backgroundColor(
      this.filter === name ? '#253247' : '#E9EDF2'
    )
    .onClick(() => {
      this.filter = name
    })
})

一种食材可以同时临期且快吃完,但页面一次只展示一个筛选条件。若需要组合筛选,filter 应改成条件集合,visible() 按启用条件依次过滤;不能新增"临期且快吃完"等大量固定按钮。

去重规则需要从名称升级

当前使用完全相等的名称判断重复,trim() 只处理首尾空白。大小写、全半角和同义名称没有归一化;家庭里也可能有两盒不同品牌牛奶,需要分别记录到期日。

更合理的模型可以让名称重复,使用ID标识记录,再加入品牌、规格和到期日期。若产品仍希望合并同类食材,重复新增时应增加数量,并明确怎样处理不同到期日;简单提示"已经在冰箱里"会丢失新买食材的信息。

数量为0为什么没有自动删除

0库存可以表达"吃完了、待补货"。自动删除会让一次误点无法恢复,也会丢失历史条目。当前减号保护下限后保留记录,用户仍能点加号恢复。

typescript 复制代码
private foodById(id: number): FoodItem | undefined {
  return this.items.find((food: FoodItem) =>
    food.id === id
  )
}

private requireFood(id: number): FoodItem {
  const target: FoodItem | undefined = this.foodById(id)
  if (!target) {
    throw new Error('Food not found: ' + id)
  }
  return target
}

数量单位也不能永远是整数:鸡蛋适合个,牛奶可能按盒,蔬菜可能按克,模型需要增加单位字段和相应步长。

临期状态不应写死为两天

当前所有食材使用统一 daysLeft <= 2。不同食品的提醒窗口可能不同,用户也可能需要自定义。可以把提醒阈值放在设置中,或给单条记录保存阈值;visible() 继续从记录与配置派生。

typescript 复制代码
private replaceFood(
  id: number,
  updater: (source: FoodItem) => FoodItem
): void {
  this.items = this.items.map((food: FoodItem) =>
    food.id === id ? updater(food) : food
  )
}

private updateFoodMessage(id: number): void {
  const target: FoodItem | undefined = this.foodById(id)
  this.message = target ? '状态已更新' : '目标记录不存在'
}

真正删除则单独确认。这样数量变化不会暗中承担删除语义,历史和分类也不会随一次减号操作消失。

库存增减统一经过边界函数

加号、减号和补货表单最后都调用同一更新入口:

typescript 复制代码
private changeStock(id: number, delta: number): void {
  this.items = this.items.map((item: FoodItem) => {
    if (item.id !== id) {
      return item
    }
    return {
      ...item,
      count: Math.max(0, item.count + delta)
    }
  })
}

private lowStock(): FoodItem[] {
  return this.items.filter((item: FoodItem) => item.count <= 1)
}
相关推荐
OPEN-F1 小时前
Python进阶教程:算法与数据结构入门
开发语言·python
薛定猫AI1 小时前
【技术干货】Claude Code多模型代理与反馈闭环:Python实现可验证的AI编程工作流
开发语言·python·ai编程
XR1234567881 小时前
工厂车间无线网络怎么选?AGV 与复杂环境是分水岭
开发语言·php
zq_63891 小时前
利用 MATLAB 调用 STK Object Model COM 显示雷达受干扰前后探测范围的变化
开发语言·matlab
Awna1 小时前
MySQL 存储空间与索引运维实战:从空间排查到在线扩容
android·运维·mysql
大黄说说1 小时前
类型安全时代:PHP 8+ 的联合类型、交集类型与泛型(模板)最佳实践
开发语言·安全·php
SWAGGY..2 小时前
【C++初阶】:(15)模板进阶--从非类型参数到模板特化与分离编译
java·服务器·前端
智购科技无人售货机工厂2 小时前
2026自动售货机触摸屏驱动开发:从I2C协议到多点触控校准的工程实践~YH
android·驱动开发·python·单片机·云原生·django
小弥儿2 小时前
GPT Image 2 提示词库,把散落的生图提示词变成工程资产
开发语言·javascript·gpt·学习