HarmonyOS 3D 拆解分析:单页多 Tab 架构的实现

发布时间:2026/7/8 2:27:34

HarmonyOS 3D 拆解分析:单页多 Tab 架构的实现 前言之前的 3D 拆解分析 App 有两个页面Index首页和AnalysisPage分析工作台。用户在首页选好场景后通过router.pushUrl跳转到分析页面。分析完按返回键回到首页。这个架构能跑但有个问题每次从分析页返回首页页面会重新渲染之前的场景选择状态会丢失。而且两个页面之间传参需要router.pushUrlrouter.getParams数据流绕了一圈。这次重构把首页和分析页合并成一个页面用底部 Tab 导航切换。不再需要页面跳转所有状态都在同一个组件里管理。架构变化旧架构两个独立页面Index.ets (首页) → router.pushUrl → AnalysisPage.ets (分析页) → router.back → Index.ets新架构单页多 TabIndex.ets ├── Tab 0: HomeTab() — 首页内容 ├── Tab 1: AnalyzeTab() — 分析工作台 └── Tab 2: HistoryTab() — 历史记录占位所有 Tab 共享同一个组件实例切换 Tab 只是改变State tab的值不会触发页面重建。底部导航栏SymbolGlyph Tab 切换导航栏布局build(){Stack({alignContent:Alignment.Bottom}){Scroll(){Column(){if(this.tab0){this.HomeTab()}elseif(this.tab1){this.AnalyzeTab()}else{this.HistoryTab()}Blank().height(90)}}Row(){this.TabBtn(0,首页)this.TabBtn(1,拆解)this.TabBtn(2,历史)}.width(100%).height(68).backgroundColor(CW).justifyContent(FlexAlign.SpaceAround).alignItems(VerticalAlign.Center).shadow({radius:12,color:rgba(0,0,0,0.05),offsetY:-2})}}用Stack让导航栏浮在内容区上面。Blank().height(90)在内容区底部留白防止最后一行内容被导航栏遮挡。Tab 按钮组件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.tabidx})}三个 Tab首页sys.symbol.house/sys.symbol.house_fill拆解sys.symbol.scope瞄准镜图标代表分析历史sys.symbol.clock时钟图标代表时间线选中态用品牌紫A未选中用淡灰#C0BFC6。图标选择逻辑privatetabIcon(idx:number,active:boolean):Resource{if(active){if(idx0)return$r(sys.symbol.house_fill)if(idx1)return$r(sys.symbol.scope)return$r(sys.symbol.clock)}if(idx0)return$r(sys.symbol.house)if(idx1)return$r(sys.symbol.scope)return$r(sys.symbol.clock)}注意scope和clock没有_fill版本或系统不支持所以选中和未选中用同一个图标只通过颜色区分。house有_fill版本选中时用实心。这种情况很常见——不是所有系统图标都有_fill变体。处理方式没有_fill的图标就用颜色区分有的就用填充颜色双重区分。状态管理从跨页传参到组件内共享旧方案路由传参// 首页privatepush(cols,rows,uri):void{router.pushUrl({url:pages/AnalysisPage,params:{gridCols:cols,...}})}// 分析页aboutToAppear(){constprouter.getParams()asRecordstring,Objectthis.colsp[gridCols]asnumber}新方案组件内状态Statetab:number0Stateuri:stringStatename:stringStatecols:number3Staterows:number3// ... 更多状态privatestartAnalysis(uri:string,c:number,r:number,n:string):void{this.uriurithis.colscthis.rowsrthis.namenthis.val0this.onfalsethis.listfalsethis.rebuildAnalysis()this.tab1// 切换到分析 Tab}startAnalysis方法做三件事设置分析数据uri、cols、rows、name重置 UI 状态val0、onfalse、listfalse切换到分析 Tab不需要路由跳转不需要参数序列化/反序列化所有数据都在同一个组件里。rebuildAnalysis从路由参数到直接调用privaterebuildAnalysis():void{constparts:PartInfo[]segmentImage(this.cols,this.rows,360,360)this.countparts.lengththis.pnpartsToNames(parts)this.pxpartsToGridX(parts)this.pypartsToGridY(parts)this.pdpartsToDepths(parts)// ... 构建网格数据和部件卡片}旧版的rebuild在aboutToAppear中调用依赖路由参数。新版的rebuildAnalysis在startAnalysis中调用直接使用组件内的State变量。首页 Tab场景选择的保留HomeTab 保留了之前的所有功能——问候语、Hero Card、统计卡片、场景预设、三步流程。唯一的区别是点击开始分析时不再跳转页面// 旧版.onClick((){this.push(this.cols(),this.rows(),)})// 新版.onClick((){this.startAnalysis(,this.colsH(),this.rowsH(),recommendGrid(this.colsH(),this.rowsH()))})startAnalysis接收四个参数图片 URI、列数、行数、物品名称。首页没有拍照功能所以 URI 传空字符串。分析 Tab从独立页面到 Tab 内容AnalyzeTab 的布局和之前的 AnalysisPage 几乎一样——头部、工作区、滑块、网格选择、部件清单。区别在于不需要aboutToAppear获取路由参数——数据已经在startAnalysis中设置好了不需要router.back()——切换回首页只需this.tab 0状态变量直接在组件内——不需要跨页面传递工作区高度固定.width(100%).height(320).margin({left:16,right:16,bottom:8})旧版的工作区用.layoutWeight(1)占满剩余空间。新版固定高度 320px——因为 Tab 切换时内容区高度不变用固定高度更可控。部件清单的紧凑化// 旧版.height(170).padding(14).borderRadius({topLeft:18,topRight:18})// 新版.height(140).padding(12).borderRadius({topLeft:16,topRight:16})部件清单的高度、内边距、圆角都缩小了。在 Tab 布局中垂直空间更宝贵——底部导航栏占了 68px内容区需要更紧凑。历史 Tab占位设计BuilderHistoryTab(){Column(){Text(分析历史).fontSize(20).fontWeight(FontWeight.Bold).fontColor(T1).width(100%).padding({left:18,top:14}).margin({bottom:12})Column(){Text().fontSize(48).opacity(0.2).margin({bottom:12})Text(暂无分析记录).fontSize(15).fontWeight(FontWeight.Medium).fontColor(T2)Text(切换到首页选择场景开始拆解).fontSize(12).fontColor(T3).margin({top:4})}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}}空状态设计标题分析历史图标48px20% 透明度主文案暂无分析记录副文案切换到首页选择场景开始拆解副文案引导用户去首页操作而不是让用户猜测这个页面是干什么的。保留 AnalysisPage 的原因新版 Index 已经包含了分析功能但AnalysisPage.ets没有删除。原因向后兼容如果有其他页面通过router.pushUrl跳转到 AnalysisPage它仍然能工作独立使用如果后续需要把分析功能单独抽出来比如做成一个可复用的组件AnalysisPage 可以作为参考渐进式迁移先在 Index 中实现新架构验证没问题后再删除旧页面踩坑记录1. Tab 切换时的状态保留if(this.tab0){this.HomeTab()}elseif(this.tab1){this.AnalyzeTab()}ArkUI 的if/else条件渲染会在条件变化时销毁旧组件、创建新组件。这意味着切换 Tab 时每个 Tab 的内部状态会丢失。如果需要保留 Tab 状态比如分析页的滑块位置有两个方案用Visibility控制显示/隐藏组件不销毁只是不可见把需要保留的状态提升到父组件当前方案——val、on等状态在 Index 层管理2. Blank().height 的高度计算Blank().height(90)这个高度需要 ≥ 导航栏高度68px 安全区域。设 90px 留了 22px 的余量。如果设太小最后一行内容会被导航栏遮挡。3. Scroll 的 edgeEffect 在 Tab 切换时Scroll(){...}.edgeEffect(EdgeEffect.Spring)Tab 切换时 Scroll 会重置滚动位置。如果需要记住每个 Tab 的滚动位置需要在切换时保存和恢复scrollOffset。4. SymbolGlyph 的选中态图标不是所有sys.symbol.*都有_fill版本。使用前最好在 DevEco Studio 的预览里确认。如果没有_fill就用颜色区分选中/未选中。写在最后单页多 Tab 架构的核心优势是状态共享——首页选的场景、分析页的滑块位置、部件清单的展开状态都在同一个组件里切换 Tab 不会丢失。代价是代码量增加了——Index.ets 现在包含了三个 Tab 的所有代码文件变长了。如果后续每个 Tab 的功能越来越复杂可以考虑把每个 Tab 拆成独立组件在 Index 中组合使用build(){Stack({alignContent:Alignment.Bottom}){Column(){if(this.tab0){HomeTab({preset:$preset})}elseif(this.tab1){AnalyzeTab({uri:$uri,...})}else{HistoryTab()}}this.BottomNav()}}这样每个 Tab 的代码独立维护Index 只负责 Tab 切换和状态管理。

相关新闻