当前位置: 首页 > news >正文

HarmonyOS今天吃什么——管理页面的增删改查与收藏设计

管理页是美食转盘的数据后台——在这里新增食物、删除食物、切换收藏。界面分三个区域:顶部输入框用于新增,中间分类标签用于筛选,底部列表展示所有食物。每个食物卡片右侧有两个操作按钮——❤️ 切换收藏,🗑️ 删除。这个页面的代码量不大,但输入验证、分类联动、列表渲染都有值得拆解的细节。

完整效果

一、管理页的三个区块

从截图看,管理页从上到下分三个区块:

区块内容功能
顶部“美食管理” 标题 + 输入框 + 添加按钮新增食物
中间分类标签横滑(全部/中餐/日料/韩餐/甜品/饮品)筛选列表
底部食物列表(emoji + 名称 + 收藏 + 删除)浏览和操作

三个区块各司其职——输入、筛选、展示完全分离。用户的操作流程是:选分类→输入名称→点添加→列表更新。

二、输入区的实现

输入框结构

Row(){TextInput({placeholder:'菜名'}).layoutWeight(2).height(40).fontSize(FontSize.sm).onChange((v:string)=>this.newName=v)TextInput({placeholder:'emoji'}).width(60).height(40).fontSize(FontSize.sm).onChange((v:string)=>this.newEmoji=v)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():Promise<void>{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.newCat===c.key?'#FFFFFF':this.gc().text).backgroundColor(this.newCat===c.key?this.gc().primary:this.gc().surface).borderRadius(BorderRadius.full).border({width:1,color:this.newCat===c.key?this.gc().primary:this.gc().border}).padding({left:Spacing.sm,right:Spacing.sm}).onClick(()=>this.newCat=c.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):Promise<void>{if(this.db){awaitthis.db.deleteFood(id);this.load();}}

直接删除,没有确认弹窗。这是因为:1)删除操作可逆——用户可以重新添加;2)历史记录不受影响——删除食物不删除历史。风险低,不需要确认。

五、空状态的设计

if(this.foods.length===0){Column(){Text('🍽️').fontSize(48)Text('还没有美食').fontSize(FontSize.md).fontColor(this.gc().textSecondary)}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)}

48px 餐盘 emoji + “还没有美食” 文字。没有引导按钮——因为输入框就在上方,用户不需要跳转就能添加。

和转盘 Tab 空状态的对比

特性转盘 Tab管理 Tab
emoji🎰 80px🍽️ 48px
文字“点击下方按钮”“还没有美食”
操作引导有按钮无按钮

管理页不需要按钮——输入框本身就是操作入口。转盘 Tab 需要按钮是因为"开始"按钮在页面底部,空状态时用户可能看不到。

六、收藏功能的完整链路

用户操作流程

用户点击 ❤️ → fav(id) 被调用 → db.toggleFav(id) 更新内存 → save() 持久化到 Preferences → load() 刷新列表 → UI 重新渲染(⭐ 出现/消失,❤️ 颜色变化)

toggleFav 的实现

asynctoggleFav(id:string):Promise<void>{for(leti=0;i<this.foods.length;i++){if(this.foods[i].id===id){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.newName=v)

每次输入变化都更新 @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 列表的标准嵌套层级——每一层都有明确职责。

http://www.jsqmd.com/news/1206266/

相关文章:

  • 【JAVA毕设源码分享】基于SpringBoot的植物知识管理与分享平台的设计与实现(程序+文档+代码讲解+一条龙定制)
  • wild-wild-path源码解析:核心函数iterate与list的实现原理
  • 小红书无水印下载工具:3种方法轻松保存你喜欢的笔记内容
  • HCL模拟器设备启动失败的6大排查方案
  • CMP技术解析:芯片制造的纳米级抛光工艺
  • DS4Windows:让PS手柄在Windows电脑上畅玩游戏的完美解决方案
  • 如何用OpenCore Legacy Patcher解决老Mac硬件兼容性问题
  • 工地放假了?别着急走!安全员请确认这7个方面和28件事已做好!
  • 军事防务:无人机集群攻击由“狼群”转换为“蜂群”
  • Qt与OpenCV结合开发图片识别应用实战指南
  • Windows Server 2008域控制器升级全攻略
  • Linux包管理工具yum详解:从基础使用到高级配置
  • 深度解密Wand-Enhancer:如何通过开源技术实现游戏修改工具的完整功能解锁
  • 让老旧电视重获新生:mytv-android安卓电视直播软件的完整指南
  • 本地大模型接入外部知识库:RAG技术实战指南
  • Linux系统ISO镜像制作指南与性能优化
  • 高级动画:弹簧动画与路径动画实战(187)
  • 别急着学 Agent 编排,权限与日志才是大厂面试的隐形门槛
  • 使用wget实现网站资源循环下载的技术指南
  • WarcraftHelper完整配置指南:5分钟让你的魔兽争霸3焕然一新!
  • PDF补丁丁:告别乱码困扰,让PDF文档在任何设备上完美显示
  • 武汉科谷技工学校办学全貌与 2026 年招生报考参考 - 升学择校早知道
  • 鸿蒙远程真机工具HOScrcpy:高效架构设计与关键技术实现方案
  • 5分钟上手:免费本地视频字幕提取神器全攻略
  • 如何用开源工具快速提取网易云与QQ音乐歌词:5大核心功能解析
  • 完整指南:使用OpenCore Legacy Patcher让旧Mac焕发新生
  • AVSession:媒体会话控制与锁屏播放(192)
  • Kimi K3 发布:2.8万亿参数开源模型,前端代码能力全球第一
  • 撕裂的认知地基:全球AI大模型的系统危机与“四重解放”路径——基于2026年7月15-17日人机对话的哲学批判与技术重构
  • 2026 年广受客户欢迎的红点奖代理公司 - 博客万