☰
买过什么、还缺什么?家庭采购清单帮忙整理
2026/10/19 4:27:55 网站建设 项目流程

家庭采购单同时显示商品分类、购买状态和完成进度。已购买与待购买不分成两份数组,而是通过每条记录的done派生;切换筛选不会复制或删除数据。

interfaceAppItem{id:numbertitle:stringgroup:stringdone:boolean}privatevisible():AppItem[]{if(this.filter==='待完成'){returnthis.items.filter((item:AppItem)=>!item.done)}if(this.filter==='已完成'){returnthis.items.filter((item:AppItem)=>item.done)}returnthis.items}

购买与撤销使用同一状态更新。进度由完整数组统计,因此在“待完成”视图里仍显示真实的总完成数量。

privatetoggle(id:number):void{this.items=this.items.map((item:AppItem)=>item.id===id?{...item,done:!item.done}:item)this.message=`已完成${this.doneCount()}/${this.items.length}`}

临时加购先检查空内容,新增项默认归类为“临时新增”。保存后筛选恢复成全部,避免新增项被当前的已完成筛选隐藏。

privateadd():void{consttitle:string=this.inputText.trim()if(title.length===0){this.message='内容不能为空'return}this.items=[...this.items,{id:Date.now(),title,group:'临时新增',done:false}]this.inputText=''this.filter='全部'}

列表中的分类文字来自group,完成样式来自done。购买状态变化时分类不应被覆盖,因此更新对象时需要把group原样带回;使用对象展开可以减少漏字段风险。

this.items=this.items.map((item:AppItem)=>item.id===id?{...item,done:!item.done}:item)

新增项放在数组末尾,符合“临时加购继续排在清单后面”的当前行为。若希望最近输入优先,应改成[item, ...items];这属于展示顺序选择,不应再维护一份排序后的状态数组。

当前记录没有数量、单价和负责人,无法计算预算小计,也不支持多人同步。增加数量与价格后,金额汇总应从未完成或全部采购项派生;多人协作则需要后端数据源和冲突处理,不能依靠组件内数组完成。

privateshoppingItemById(id:number):AppItem|undefined{returnthis.items.find((shoppingItem:AppItem)=>shoppingItem.id===id)}privaterequireShoppingItem(id:number):AppItem{consttarget:AppItem|undefined=this.shoppingItemById(id)if(!target){thrownewError('ShoppingItem not found: '+id)}returntarget}

顶部完成数和进度条始终读取完整采购数组。切换到“待完成”后,列表数量会减少,但进度仍表示整张采购单。

Text(`${this.doneCount()}/${this.items.length}`)Progress({value:this.doneCount(),total:this.items.length,type:ProgressType.Linear})Row({space:7}){ForEach(['全部','待完成','已完成'],(name:string)=>{Button(name).onClick(()=>{this.filter=name})})}

状态切换时需要保留分类。当前工程显式复制id、title、group、done四个字段;字段增加后,这种写法容易漏字段,可以在重构时改用对象展开,只覆盖done。

privatereplaceShoppingItem(id:number,updater:(source:AppItem)=>AppItem):void{this.items=this.items.map((shoppingItem:AppItem)=>shoppingItem.id===id?updater(shoppingItem):shoppingItem)}privateupdateShoppingItemMessage(id:number):void{consttarget:AppItem|undefined=this.shoppingItemById(id)this.message=target?'状态已更新':'目标记录不存在'}

多人协作不是把数组持久化就能完成。同一商品被两个人同时勾选时,需要服务端版本、操作时间或冲突合并规则;当前组件状态只覆盖单设备操作。

完成数量不是独立计数器

采购项只保存一份done,顶部进度从数组过滤得到。勾选时按 ID 返回新对象:

privatedoneCount():number{returnthis.items.filter((item:AppItem)=>item.done).length}privatetoggle(id:number):void{this.items=this.items.map((item:AppItem)=>{if(item.id!==id){returnitem}return{id:item.id,title:item.title,group:item.group,done:!item.done}})}

取消勾选会自然减少完成数,新增项会同时增加总数。若保存一个单独计数器,新增、删除和恢复购买状态都必须记得修正,数据很容易分裂。派生统计让所有变更只落在记录数组上。

分类筛选也只能返回视图集合,勾选仍写回源数组。不能复制“待购买”和“已购买”两份列表,再在勾选时搬运对象;移动过程中会丢失排序、分类或备注。一个状态字段配合过滤函数已经足够表达两个视图。

"2袋牛奶"和"2升牛奶"不能只存一个数字

当前条目只有名称和分类,适合验证清单交互。真实采购需要数量、单位、预算单价、实际单价和购买人。数量应为正数,单位从受控选项选择;“2袋牛奶”和“2升牛奶”不能只保存一个数字。

privateremoveShoppingItem(id:number):void{constbefore:number=this.items.lengththis.items=this.items.filter((shoppingItem:AppItem)=>shoppingItem.id!==id)this.message=this.items.length<before?'记录已删除':'没有找到要删除的记录'}privatehasShoppingItem(id:number):boolean{returnthis.items.some((shoppingItem:AppItem)=>shoppingItem.id===id)}

购买完成不一定等于任务结束。用户勾选时可以展开实际数量和价格,确认后写done、购买时间与购买人;快速勾选则使用计划值。撤销完成时是否保留已填价格需要明确,通常可以暂存但不计入实付。

牛奶已经有了,还添不添

新增入口去除空白后再创建对象。家庭中“牛奶”可能已经存在,直接拒绝重复不一定合适;可以提示合并数量、仍然新增或取消。ID 才是唯一性,名称只是业务判断依据。

privateshoppingItemKey(shoppingItem:AppItem):string{returnString(shoppingItem.id)}privateallShoppingItemKeys():string[]{returnthis.items.map((shoppingItem:AppItem)=>this.shoppingItemKey(shoppingItem))}privatehasDuplicateShoppingItemKey():boolean{constkeys:string[]=this.allShoppingItemKeys()returnnewSet(keys).size!==keys.length}

这套单数组设计为数量、预算和协作扩展提供了可靠基础。每个功能继续围绕稳定 ID 和明确字段增加,而不是为每种页面状态复制一份采购数据。

从单机状态验证到同步验证

单机用例连续执行新增、勾选、取消、分类过滤和归档,检查完成数量始终等于源数组中done为真的条目数。金额字段加入后,预算与实付分别按定义聚合,0.01 元等小额仍以整数分计算。空批次和筛选空结果使用不同提示。

privatefilterShoppingItem(predicate:(source:AppItem)=>boolean):AppItem[]{returnthis.items.filter((shoppingItem:AppItem)=>predicate(shoppingItem))}privatecountShoppingItem(predicate:(source:AppItem)=>boolean):number{returnthis.filterShoppingItem(predicate).length}

离线重连后先合并再刷新页面,不能用远端整数组直接覆盖本地未上传变化。

同步层提交的是目标状态

页面点击先算出目标值,同步层提交幂等更新:

privatesetPurchased(id:string,purchased:boolean):void{this.items=this.items.map((item:ShoppingItem)=>item.id===id?{...item,purchased,updatedAt:Date.now()}:item)}privatebuildPatch(item:ShoppingItem):ShoppingPatch{return{id:item.id,purchased:item.purchased,version:item.version}}

同一个请求重试两次仍得到相同状态,不会像远端执行两次 toggle 那样翻回原值。

调试日志:一次勾选到底改了什么

单设备状态下勾选出错,肉眼只能看到"计数没动"这一个结果,看不出是数组没换还是读数没刷新。给toggle()前后各加一行日志,把关键状态打出来再跑:

privatedebugToggle(id:number):void{constbefore:AppItem=this.requireShoppingItem(id)console.log(`[toggle] id=${id}before done=${before.done}`+`count=${this.doneCount()}len=${this.items.length}`)this.items=this.items.map((item:AppItem)=>item.id===id?{...item,done:!item.done}:item)constafter:AppItem=this.requireShoppingItem(id)console.log(`[toggle] id=${id}after done=${after.done}`+`count=${this.doneCount()}len=${this.items.length}`)}

勾选"牛奶两盒"时正常输出是两行:第一行done=false count=0 len=3,第二行done=true count=1 len=3。count 变而 len 不变,说明只翻转了状态、没有增删记录。

有一版代码在map()里同时读了局部缓存的旧数组,日志打出来 before 和 after 的done都是 false,连点两次也只算一次。把数据源统一成this.items之后日志才恢复正常。这类问题不用日志很难定位,因为界面上的表现只是"按钮时好时坏"。

清单之后还能挂什么

采购项加上数量、单位和价格之后,清单就能算出预算小计和实付差额:sum(planPrice)对未完成项求和是预算,sum(actualPrice)对已完成项求和是实付。两个数字都从同一份数组派生,不额外存总额。

再往外是分类汇总和负责人口径。按group分组求和可以回答"这个月日用花了多少";给每条记录加owner字段可以回答"谁买的东西超支了"。这些功能都不需要改数据流主线,仍然是围绕稳定 ID 和明确字段做扩展,不为每种页面状态复制一份采购数据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询