HarmonyOS 3D 拆解分析:Tab 导航记忆与发现页交互闭环

HarmonyOS 3D 拆解分析:Tab 导航记忆与发现页交互闭环
前言上一版的发现页有一个体验问题点击一个拆解卡片后跳到拆解页拆解完想回到发现页得先回首页再切到发现。因为拆解页的返回按钮是硬编码this.tab 0回首页不管你从哪个 Tab 进来的。这次改动解决两个问题返回按钮记住你从哪来的——从发现页进来就回发现页从首页进来就回首页发现卡片可点击——点一个 iPhone 拆解直接带着 4×3 网格参数进入拆解页prevTab导航记忆的核心新增状态变量Statetab:number0StateprevTab:number0// 新增记录上一个 TabprevTab只做一件事记录用户切换 Tab 之前在哪个页面。TabBtn 的变化BuilderTabBtn(idx:number,label:string){Column({space:3}){SymbolGlyph(this.tabIcon(idx,this.tabidx)).fontSize(22).fontColor([this.tabidx?A:#C0BFC6])Text(label).fontSize(10).fontColor(this.tabidx?A:#C0BFC6)}.onClick((){this.prevTabthis.tab;this.tabidx})// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^}关键变化onClick 里先this.prevTab this.tab再this.tab idx。这行代码的执行顺序很重要this.prevTab this.tab—— 把当前 Tab 存到 prevTabthis.tab idx—— 切换到新 Tab比如用户在发现页tab2点击历史 Tabidx3prevTab 2发现页tab 3历史页之后在历史页点击返回按钮this.tab this.prevTab→ tab 2回到发现页startAnalysis 也记录 prevTabprivatestartAnalysis(uri:string,c:number,r:number,n:string):void{this.uriuri;this.colsc;this.rowsr;this.namenthis.val0;this.onfalse;this.listfalsethis.rebuildAnalysis()this.prevTabthis.tab// 记录当前 Tabthis.tab1// 切到拆解页}从首页点开始使用 → prevTab0tab1从发现页点卡片 → prevTab2tab1拆解页的返回按钮根据 prevTab 决定去哪。返回按钮的统一改造拆解页// 旧版.onClick((){this.tab0})// 永远回首页// 新版.onClick((){this.tabthis.prevTab})// 回到上一个 Tab发现页// 旧版.onClick((){this.tab0})// 新版.onClick((){this.tabthis.prevTab})历史页// 旧版.onClick((){this.tab0})// 新版.onClick((){this.tabthis.prevTab})三个子页面的返回按钮逻辑完全一致回到 prevTab。为什么不用 router.back()因为这是单页应用——所有 Tab 都在同一个Index组件里没有路由栈。Tab 切换只是this.tab的值变化不涉及页面压栈/出栈。所以不能用router.back()只能用状态变量控制。发现卡片的数据升级DiscoverItem 接口// 旧版interfaceDiscoverItem{title:string;desc:string;icon:string;likes:string;color:string}// 新版interfaceDiscoverItem{title:string;desc:string;icon:string;likes:string;color:string;cols:number;rows:number// 新增网格参数}新增cols和rows字段让每个发现项目携带自己的网格配置。Mock 数据privategetMockDiscover():DiscoverItem[]{return[{title:iPhone 15 拆解,desc:...,icon:,likes:128,color:#FF6B6B,cols:4,rows:3},{title:MacBook 主板分析,desc:...,icon:,likes:96,color:#5DADE2,cols:4,rows:4},{title:AirPods 内部结构,desc:...,icon:,likes:203,color:#00B894,cols:2,rows:3},{title:Switch 手柄拆解,desc:...,icon:,likes:74,color:#FECA57,cols:3,rows:3},{title:机械键盘轴体,desc:...,icon:⌨️,likes:51,color:A,cols:3,rows:3},{title:无人机电路分析,desc:...,icon:,likes:37,color:#6C5CE7,cols:4,rows:3}]}每个项目的网格参数设备网格部件数为什么这么配iPhone4×312手机零件多需要较细的切分MacBook4×416主板芯片密集最细网格AirPods2×36微型设备粗网格就够Switch3×39中等复杂度键盘3×39轴体排列规整无人机4×312电路板机械结构发现卡片的交互闭环旧版只读.onClick((){// Navigate to analysis detail (future)})点击卡片什么也不做——只有占位注释。新版可操作.onClick((){this.startAnalysis(,item.cols,item.rows,item.title)})点击卡片直接进入拆解页携带该项目的网格参数和标题。交互流程发现页 → 点击iPhone 15 拆解 → 拆解页4×3 网格标题iPhone 15 拆解 ↓ 点击返回按钮 ↓ 回到发现页prevTab2为什么uri传空字符串this.startAnalysis(,item.cols,item.rows,item.title)uri是图片路径。发现页的卡片是别人分享的拆解不需要自己的照片。传空字符串时拆解页会显示网格预览而不是真实图片——用户看到的是这个拆解用了什么网格配置而不是这张照片长什么样。发现页头部重设计旧版标题和状态指示器分开Text(发现拆解).fontSize(20).fontWeight(FontWeight.Bold)// ... 中间有空白 ...Row(){Row(){}.width(6).height(6).borderRadius(3).backgroundColor(this.discoverLoading?#FECA57:#00B894)Text(this.discoverLoading? 加载中...: 在线).fontSize(11)}标题和状态指示器不在同一行视觉上比较散。新版标题和状态指示器同行Row(){// 返回按钮Row(){SymbolGlyph($r(sys.symbol.chevron_left))...}.width(34).height(34).borderRadius(17).backgroundColor(rgba(0,0,0,0.03)).justifyContent(FlexAlign.Center).onClick((){this.tabthis.prevTab})Text(发现拆解).fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1).margin({left:10})Blank()// 在线状态指示器紧凑版Row(){Row(){}.width(6).height(6).borderRadius(3).backgroundColor(this.discoverLoading?#FECA57:#00B894)Text( 在线).fontSize(11).fontColor(T3)}.padding({left:8,right:8,top:4,bottom:4}).backgroundColor(#F5F5F7).borderRadius(10)}变化标题和返回按钮、状态指示器在同一行状态指示器加了灰色背景和圆角——从文字变成了徽章去掉了加载中…文字——只有在线一个状态加载中的时候显示黄色圆点热门拆解横幅// Featured bannerStack({alignContent:Alignment.Center}){Column(){}.width(100%).height(100).borderRadius(18).linearGradient({angle:135,colors:[[A,0],[#A78BFA,1]]})Circle({width:80,height:80}).fill(rgba(255,255,255,0.06)).position({x:70%,y:-20})Circle({width:40,height:40}).fill(rgba(255,255,255,0.08)).position({x:5%,y:50})Row(){Column(){Text( 热门拆解).fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White)Text(看看大家都在拆什么).fontSize(11).fontColor(rgba(255,255,255,0.6)).margin({top:2})}.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({left:18})Text().fontSize(36).margin({right:18})}.width(100%)}.width(100%).height(100).borderRadius(18).margin({left:18,right:18,bottom:18})横幅的结构渐变背景紫色渐变和 Hero Card 一致装饰圆两个半透明白色圆营造层次感文字标题热门拆解 副标题看看大家都在拆什么右侧 emoji 作为装饰为什么加横幅发现页之前直接就是卡片列表没有视觉焦点。加一个横幅给页面一个起点——用户进入发现页第一眼看到的是横幅然后自然向下滚动看卡片横幅可以后续做成轮播推荐不同的内容和首页的 Hero Card 风格一致——整个 App 的视觉语言统一底部提示文案// 旧版Text(—— 上拉加载更多 ——).fontSize(12).fontColor(T3)// 新版Row(){Text(———— 已经到底了 ————).fontSize(11).fontColor(T3)}.width(100%).justifyContent(FlexAlign.Center).margin({top:12,bottom:30})从上拉加载更多改成已经到底了——因为当前没有分页功能 mock 数据只有 6 条。告诉用户到底了比上拉加载更诚实。后续实现分页时可以动态切换Text(this.hasMore?上拉加载更多:已经到底了)踩坑记录1. prevTab 的初始值StateprevTab:number0初始值是 0首页。这意味着 App 启动时如果用户直接切到拆解页再返回会回到首页——因为 prevTab 默认是 0。这个默认值是合理的App 启动时在首页第一次进入拆解页一定是从首页来的。2. startAnalysis 和 TabBtn 都写 prevTab// TabBtn.onClick((){this.prevTabthis.tab;this.tabidx})// startAnalysisthis.prevTabthis.tabthis.tab1两处都写 prevTab但语义不同TabBtn用户主动切换 TabprevTab 记录刚才在哪个 TabstartAnalysis程序主动跳转点击卡片/按钮prevTab 记录触发跳转时在哪个 Tab如果只在一处写 prevTab另一处就会丢失记忆。3. 发现卡片的 onClick 传空 urithis.startAnalysis(,item.cols,item.rows,item.title)传空 uri 时拆解页显示网格预览而不是真实图片。但用户可能期望点击一个拆解项目能看到完整的爆炸图。后续可以考虑在 DiscoverItem 里加imageUri字段或者跳到一个独立的详情页而不是拆解页4. 横幅没有点击事件Stack({alignContent:Alignment.Center}){// ...}// 没有 .onClick()当前横幅是纯展示不能点击。如果后续要做热门推荐详情需要加 onClick。写在最后prevTab是一个很小的改动——只加了一个状态变量改了几行 onClick。但它解决了一个真实的体验问题用户不再需要绕路回到之前的页面。发现卡片的交互闭环让发现页从只读列表变成了操作入口。用户看到一个感兴趣的拆解点一下就能用自己的照片尝试同样的网格配置——这是从看别人做什么到自己动手做的关键转化。后续如果要做分享我的拆解功能DiscoverItem 还需要加imageUri、author、timestamp等字段。但核心的导航记忆和交互闭环已经搭好了。