库存页需要同时回答三个问题:冰箱里有什么、哪些快到期、哪些快吃完。把三类结果分别保存成数组,会让新增和数量修改变得难以同步;这里保留一份 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 不可编辑,因此临期数量只随新增数据变化。接入真实日期后,应保存到期时间,由当天日期派生剩余天数;每天打开应用都能得到新结果,无需每天批量改写全部记录。
三个筛选按钮共享可见函数
按钮只更新 filter,List 始终遍历 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)
}