HarmonyOS购物商城——首页交互优化与细节打磨
首页架构之前拆过一次当时重点在布局结构和模块拆分。这次的改动不大但每个改动都指向一个具体的问题底部导航的高亮逻辑太复杂、搜索图标点击没校验、消息Tab的图标选错了。三个小改动解决三个实际体验问题。完整效果底部导航去掉 tab 状态变量上一版的问题上一版的底部导航用一个State tab变量控制高亮State tab: number 0 Builder HomeBtn() { Column({ space: 2 }) { SymbolGlyph(this.tab 0 ? $r(sys.symbol.house_fill) : $r(sys.symbol.house)) .fontSize(22).fontColor([this.tab 0 ? A : #C0BFC6]) Text(首页).fontSize(9).fontColor(this.tab 0 ? A : #C0BFC6) }.onClick(() { this.tab 0 }) }每个Tab都要判断this.tab idx决定用哪个图标、什么颜色。五个Tab就要写五次判断逻辑。新版的做法新版移除了State tab每个Tab直接写死高亮状态Builder HomeBtn() { Column({ space: 2 }) { SymbolGlyph($r(sys.symbol.house_fill)).fontSize(22).fontColor([A]) Text(首页).fontSize(9).fontColor(A) } } Builder CategoryBtn() { Column({ space: 2 }) { SymbolGlyph($r(sys.symbol.scope)).fontSize(22).fontColor([#C0BFC6]) Text(分类).fontSize(9).fontColor(#C0BFC6) }.onClick(() { router.pushUrl({ url: pages/ProductList, ... }) }) }HomeBtn永远是高亮状态红色fill图标其他Btn永远是未激活状态灰色普通图标。点击其他Tab时直接跳转页面而不是切换tab状态。为什么这样做这个项目的首页是独立页面不是单页面多Tab切换。用户点击分类会跳转到ProductList页面点击购物车会跳转到CartPage页面点击我的会跳转到ProfilePage页面。每个Tab对应一个独立的路由页面不存在在同一个页面内切换Tab的场景。上一版用State tab控制高亮隐含了一个假设“用户可能在同一个页面内切换Tab”。但实际的路由架构是页面级跳转这个状态变量就多余了。去掉State tab的好处减少状态管理不需要维护一个随时会过时的状态变量避免高亮错乱用户从ProfilePage返回首页时tab状态可能还停留在4“我的”首页的HomeBtn不会高亮代码更简单每个Builder只需要写自己的样式不需要判断条件什么时候需要 tab 状态如果首页是单页面多Tab架构类似微信底部导航用户点击Tab不跳转页面只是切换同一页面内的内容区域——这时就需要State tab来控制哪个Tab高亮。判断标准很简单点击Tab后是跳转到新页面router.pushUrl还是在当前页面切换内容前者不需要tab状态后者需要。搜索图标空值守卫上一版的问题上一版的搜索图标点击逻辑.onClick(() { router.pushUrl({ url: pages/ProductList, params: { search: this.searchText } }) })没有校验this.searchText是否为空。用户没输入任何内容点击搜索图标也会跳转到ProductList页面带着一个空字符串的search参数。跳转到空搜索页面的后果ProductList页面需要处理空搜索的情况显示全部商品显示提示用户看到空搜索结果会困惑“我什么都没搜为什么跳到这里”多一次无意义的页面跳转浪费路由栈空间新版的解法.onClick(() { if (this.searchText.trim().length 0) { router.pushUrl({ url: pages/ProductList, params: { search: this.searchText } }) } })加了一个简单的守卫this.searchText.trim().length 0。trim去掉首尾空格后如果长度大于0才跳转。空字符串或纯空格不会触发跳转。为什么用 trim直接判断this.searchText.length 0不够——用户可能输入了几个空格length大于0但实际内容是空的。trim()去掉首尾空格后再判断更严谨。但trim()不会去掉中间的空格。如果用户输入a btrim后是a blength是3会正常跳转。这是合理的行为——中间有空格说明用户确实想搜索a b。搜索栏的 onSubmit 不需要改搜索栏的TextInput有onSubmit事件用户按回车时触发.onSubmit(() { router.pushUrl({ url: pages/ProductList, params: { search: this.searchText } }) })这里没有加空值守卫。原因是TextInput的onSubmit在用户主动按回车时才触发而搜索图标的onClick在用户点击时触发。用户按回车说明他已经输入了内容否则按回车没意义所以不需要校验。但用户可能不小心点击了搜索图标比如想点击标题但点偏了所以图标需要校验。消息Tab的图标修正上一版的问题上一版的消息Tab用的是放大镜图标Builder MsgBtn() { Column({ space: 2 }) { SymbolGlyph($r(sys.symbol.magnifyingglass)).fontSize(22).fontColor([#C0BFC6]) Text(消息).fontSize(9).fontColor(#C0BFC6) } }放大镜是搜索的通用符号用在消息Tab上会造成认知冲突——用户看到放大镜会下意识觉得这是搜索但文字写的是消息。图标和文字的语义不一致增加用户的认知负担。新版的修正Builder MsgBtn() { Column({ space: 2 }) { SymbolGlyph($r(sys.symbol.heart)).fontSize(22).fontColor([#C0BFC6]) Text(消息).fontSize(9).fontColor(#C0BFC6) } }改成了心形图标heart。心形在社交类App中通常代表收藏或喜欢用在消息Tab上也不完全准确但至少不会和搜索混淆。图标选型的取舍理想的消息图标应该是铃铛bell或对话气泡bubble。但鸿蒙系统SymbolGlyph的图标库不一定有这些图标开发者只能从可用的图标中选最接近的。sys.symbol.heart虽然不完美但有三个优点不和搜索混淆视觉上是实心的fill和其他Tab的图标风格一致用户看到心形不会产生这是搜索的误解在完美但不可用和不完美但可用之间选后者。这是移动端开发的常见妥协——系统图标库有限只能在现有资源中找最优解。三个改动的共同逻辑这三个改动看似独立但背后有一个共同的思路减少用户的认知负担。底部导航去掉tab状态 → 避免首页明明亮着但高亮不对的困惑搜索图标加空值守卫 → 避免我什么都没搜但跳到了搜索页的困惑消息Tab换图标 → 避免放大镜不是搜索吗怎么是消息的困惑每个改动都不大但都解决了一个让用户愣一下的问题。好的UI不是没有bug是用户不会因为UI而产生困惑。踩坑记录SymbolGlyph的fontColor需要用数组。fontColor([A])而不是fontColor(A)。SymbolGlyph的fontColor接受ResourceColor[]类型必须传数组。如果传单个颜色值编译不会报错但运行时图标可能不显示颜色。这是鸿蒙API的一个隐蔽坑——类型签名不够严格错误不会在编译期暴露。router.pushUrl的params类型断言。params: { search: this.searchText } as Recordstring,Object需要as断言。ArkUI的路由API对params类型有严格要求直接传对象会有类型错误。Recordstring,Object是最宽松的类型能接受任何键值对。实际项目中可以定义具体的params接口但原型阶段用Record更灵活。** Blank().height(80) 的高度计算。** 底部留白80px 导航栏高度64px 安全间距16px。如果以后导航栏高度变了比如改成72px这个留白也需要同步调整。建议用常量管理const NAV_HEIGHT 64留白高度用NAV_HEIGHT 16计算。