HarmonyOS今天吃什么——管理页面的增删改查与收藏设计
管理页是美食转盘的数据后台——在这里新增食物、删除食物、切换收藏。界面分三个区域顶部输入框用于新增中间分类标签用于筛选底部列表展示所有食物。每个食物卡片右侧有两个操作按钮——❤️ 切换收藏️ 删除。这个页面的代码量不大但输入验证、分类联动、列表渲染都有值得拆解的细节。完整效果一、管理页的三个区块从截图看管理页从上到下分三个区块区块内容功能顶部“美食管理” 标题 输入框 添加按钮新增食物中间分类标签横滑全部/中餐/日料/韩餐/甜品/饮品筛选列表底部食物列表emoji 名称 收藏 删除浏览和操作三个区块各司其职——输入、筛选、展示完全分离。用户的操作流程是选分类→输入名称→点添加→列表更新。二、输入区的实现输入框结构Row(){TextInput({placeholder:菜名}).layoutWeight(2).height(40).fontSize(FontSize.sm).onChange((v:string)this.newNamev)TextInput({placeholder:emoji}).width(60).height(40).fontSize(FontSize.sm).onChange((v:string)this.newEmojiv)Text(添加).fontSize(FontSize.sm).fontColor(this.newName?this.gc().primary:this.gc().textSecondary).margin({left:Spacing.sm}).onClick(()this.addFood())}.width(100%).padding({left:Spacing.md,right:Spacing.md,bottom:Spacing.sm})两个 TextInput 的分工输入框占比用途必填菜名layoutWeight(2)输入食物名称是emojiwidth(60)输入表情符号否默认 ️菜名框占 2/3 宽度emoji 框固定 60px——因为菜名通常比 emoji 长。布局比例跟着内容长度走。添加按钮的状态.fontColor(this.newName?this.gc().primary:this.gc().textSecondary)有输入内容时按钮变主色可点击无内容时变灰色不可用。不需要禁用 onClick——在 addFood 方法开头做校验即可。颜色变化只是视觉提示。addFood 方法privateasyncaddFood():Promisevoid{if(!this.newName.trim()||!this.db)return;awaitthis.db.addFood(createFoodItem(this.newName.trim(),this.newCat,this.newEmoji||️));this.newName;this.newEmoji;this.load();}三步操作校验→写入→清空。写入后立即清空输入框——用户可以连续添加多个食物不需要手动删除上一次的输入。输入校验if(!this.newName.trim()||!this.db)return;trim() 去除首尾空格——防止用户输入纯空格。this.db的空值检查防止数据库未初始化时崩溃。三、分类筛选的设计分类标签的结构Scroll(){Row(){ForEach(CATEGORIES.slice(1),(c:FoodCategory){Text(c.emoji c.name).fontSize(FontSize.xs).height(28).fontColor(this.newCatc.key?#FFFFFF:this.gc().text).backgroundColor(this.newCatc.key?this.gc().primary:this.gc().surface).borderRadius(BorderRadius.full).border({width:1,color:this.newCatc.key?this.gc().primary:this.gc().border}).padding({left:Spacing.sm,right:Spacing.sm}).onClick(()this.newCatc.key).margin({right:Spacing.xs})})}}.scrollable(ScrollDirection.Horizontal)CATEGORIES.slice(1) 的含义CATEGORIES.slice(1)// 跳过第一个全部分类管理页的分类标签不含全部——因为新增食物必须选择具体分类。全部只在转盘 Tab 用于筛选在管理页没有意义——你不能给一个食物分配全部分类。和转盘 Tab 的分类标签对比特性转盘 Tab管理 Tab数据源CATEGORIES.slice(0, 6)CATEGORIES.slice(1)含全部是否选中态行为筛选转盘范围决定新增食物的分类变量pickCatnewCat同一个 CATEGORIES 常量两个 Tab 用不同的 slice 范围。转盘需要全部来不限分类管理页不需要。分类筛选的视觉设计选中态白色文字 主色背景 主色边框。未选中深色文字 surface 背景 边框色边框。这是贯穿整个 App 的 Chip 选中模式——和转盘 Tab 的筛选标签完全一致。四、食物列表的渲染列表结构List(){ForEach(this.foods,(f:FoodItem){ListItem(){Row(){Text(f.emoji).fontSize(24)Text(f.name).fontSize(FontSize.md).fontColor(this.gc().text).margin({left:Spacing.sm})if(f.isFavorite)Text(⭐).fontSize(14)Blank()Text(❤️).fontSize(18).fontColor(f.isFavorite?#EF4444:this.gc().border).onClick(()this.fav(f.id)).margin({right:Spacing.sm})Text(️).fontSize(16).onClick(()this.del(f.id))}.width(100%).padding(Spacing.md).backgroundColor(this.gc().surface).borderRadius(BorderRadius.md).margin({bottom:Spacing.sm,left:Spacing.md,right:Spacing.md})}})}每行的元素排列[emoji] [名称] [⭐收藏标记] ── Blank ── [❤️收藏按钮] [️删除按钮]左侧是信息emoji 名称右侧是操作收藏 删除。中间用 Blank 撑开——信息左对齐操作右对齐。收藏标记的两种显示状态⭐ 标记❤️ 按钮颜色已收藏显示红色 (#EF4444)未收藏不显示边框色 (灰色)⭐ 是静态标记——只在已收藏时显示。❤️ 是交互按钮——点击切换收藏状态。两个元素配合⭐ 告诉用户这个已收藏❤️ 让用户操作取消/添加收藏。收藏按钮的颜色逻辑.fontColor(f.isFavorite?#EF4444:this.gc().border)已收藏时红色未收藏时灰色。红色 “激活状态”灰色 “未激活”。用户一眼就能区分哪些食物已收藏。删除按钮的视觉处理Text(️).fontSize(16).onClick(()this.del(f.id))删除按钮没有特殊颜色——用默认的 ️ emoji。这是刻意的设计——如果删除按钮用红色会和收藏按钮冲突用户可能误操作。用 emoji 保持中性——不引导用户删除。删除操作privateasyncdel(id:string):Promisevoid{if(this.db){awaitthis.db.deleteFood(id);this.load();}}直接删除没有确认弹窗。这是因为1删除操作可逆——用户可以重新添加2历史记录不受影响——删除食物不删除历史。风险低不需要确认。五、空状态的设计if(this.foods.length0){Column(){Text(️).fontSize(48)Text(还没有美食).fontSize(FontSize.md).fontColor(this.gc().textSecondary)}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center)}48px 餐盘 emoji “还没有美食” 文字。没有引导按钮——因为输入框就在上方用户不需要跳转就能添加。和转盘 Tab 空状态的对比特性转盘 Tab管理 Tabemoji 80px️ 48px文字“点击下方按钮”“还没有美食”操作引导有按钮无按钮管理页不需要按钮——输入框本身就是操作入口。转盘 Tab 需要按钮是因为开始按钮在页面底部空状态时用户可能看不到。六、收藏功能的完整链路用户操作流程用户点击 ❤️ → fav(id) 被调用 → db.toggleFav(id) 更新内存 → save() 持久化到 Preferences → load() 刷新列表 → UI 重新渲染⭐ 出现/消失❤️ 颜色变化toggleFav 的实现asynctoggleFav(id:string):Promisevoid{for(leti0;ithis.foods.length;i){if(this.foods[i].idid){this.foods[i].isFavorite!this.foods[i].isFavoritebreak}}awaitthis.save()}直接修改 isFavorite 属性——布尔值取反。不需要创建新对象——只改一个字段。收藏状态的持久化收藏状态存在食物对象的 isFavorite 字段里——和其他字段一起序列化到 Preferences。不需要单独的收藏表——收藏是食物的一个属性不是独立数据。七、列表的布局参数每行的间距.padding(Spacing.md)// 内边距 16px.margin({bottom:Spacing.sm,left:Spacing.md,right:Spacing.md})上下间距不对称——padding 提供内边距margin-bottom 提供行间距。实际行间距 padding(16) marginBottom(8) 24px。左右 margin 让列表不贴边。列表的滚动配置.width(100%).layoutWeight(1).scrollBar(BarState.Off)scrollBar(BarState.Off) 隐藏滚动条——保持界面整洁。列表用 layoutWeight(1) 占据剩余空间——输入框和分类标签在上方固定列表在下方滚动。八、State 变量的分工变量作用触发 UI 刷新foods食物列表load()pickCat分类筛选转盘用load()newCat新增食物的分类分类标签高亮newName新增食物的名称按钮颜色变化newEmoji新增食物的 emoji无只在添加时读取newName 的双重作用newName 既控制按钮颜色又是新增食物的参数。用户每输入一个字符按钮颜色可能从灰色变为主色——这是实时响应的 UI 反馈。newEmoji 的默认值处理this.newEmoji||️用户不输入 emoji 时用 ️ 兜底——保证每个食物都有表情。这个逻辑在 addFood 方法里不在 onChange 里——不干扰用户的输入体验。九、输入框的交互细节TextInput 的 onChange 时机TextInput({placeholder:菜名}).onChange((v:string)this.newNamev)每次输入变化都更新 State——实时同步。不是失去焦点时才更新——用户边输入边看到按钮颜色变化。输入框的样式.height(40).fontSize(FontSize.sm)40px 高度 12px 字号——紧凑型输入框。管理页的输入框不需要太大——它不是主要内容只是一个快速添加入口。emoji 输入框的宽度.width(60)60px 固定宽度——emoji 通常只有 1-2 个字符。不需要 layoutWeight——emoji 输入框的长度可预测。十、整体嵌套结构Column ManageTab 容器 ├→ Text (美食管理) 标题 ├→ Row (输入区) TextInput×2 添加按钮 ├→ Scroll Row (分类标签) 横向滚动的分类选择 └→ 条件渲染 ├→ 空状态 Column 无食物时 └→ List ForEach 有食物时 └→ ListItem Row 每行emoji 名称 收藏 删除嵌套深度 4 层——List → ListItem → Row → Text。这是 ArkTS 列表的标准嵌套层级——每一层都有明确职责。