news 2026/7/29 2:21:58

HarmonyOS NEXT 企业级记账APP:实现底部 Tab 导航

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS NEXT 企业级记账APP:实现底部 Tab 导航

实现底部 Tab 导航

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第04篇,对应 Git Tagv0.0.4。承接第 03 篇的主题系统,本篇使用 ArkUI 的 Tabs 组件搭建首页、统计、预算、我的四个底导航 Tab,并封装 AppTabBar 通用组件。

前言

底导航 Tab 是移动应用的核心导航模式,用户 80% 的操作都从底导航发起。HarmonyLedger 规划四个主 Tab:首页、统计、预算、我的。ArkUI 提供原生Tabs组件支持底导航,但企业级项目需要封装统一的 AppTabBar 组件,便于后续扩展与主题适配。

本文将带你:

  1. 使用 ArkUI Tabs 组件搭建底导航框架
  2. 封装 AppTabBar 通用组件(支持图标、文字、主题色)
  3. 实现四个 Tab 页面占位与切换动效
  4. 集成主题系统响应深色模式

企业级核心原则:底导航必须封装复用、禁止重复、支持主题。参考 ArkUI Tabs 组件 了解官方约定。


一、Tab 规划

1.1 四个主 Tab

HarmonyLedger 选用最经典的四 Tab布局,覆盖核心业务场景:

Tab 序号名称图标页面业务
0首页homeHomeView账单汇总、快捷新增
1统计chartStatisticsView收支图表分析
2预算budgetBudgetView预算设置与进度
3我的userProfileView设置、关于

1.2 Tab 命名规范

Tab 命名规范: - 英文小写下划线:home / chart / budget / user - 中文展示用:首页 / 统计 / 预算 / 我的 - 图标资源:icon_tab_{name}.png - 路由页面:{Name}View.ets(大驼峰) - 资源字符串:tab_{name}_label

设计要点:Tab 命名一旦确定禁止修改,否则会牵动图标、路由、字符串多处资源。


二、Tabs 组件基础

2.1 Tabs 与 TabContent

ArkUI 原生 Tabs 组件使用方式:

@Entry @Component struct MainView { @State currentIndex: number = 0; build() { Tabs({ index: this.currentIndex }) { TabContent() { HomeView() } .tabBar({ text: '首页', icon: $r('app.media.icon_tab_home') }) TabContent() { StatisticsView() } .tabBar({ text: '统计', icon: $r('app.media.icon_tab_chart') }) TabContent() { BudgetView() } .tabBar({ text: '预算', icon: $r('app.media.icon_tab_budget') }) TabContent() { ProfileView() } .tabBar({ text: '我的', icon: $r('app.media.icon_tab_user') }) } .barPosition(BarPosition.End) .onChange((index: number) => { this.currentIndex = index; }) } }

2.2 关键属性

属性说明
barPositionTab 位置BarPosition.End(底导航)/Start(顶导航)
vertical是否纵向false(默认横向)
scrollable可滚动false(四 Tab 不需滚动)
onChange切换回调(index: number) => void
tabBarTab 标签配置{ text, icon }

踩坑点TabContent不能直接传 builder 参数,必须用.tabBar(...)设置标签。这是鸿蒙 ArkUI 的强制约束。


三、封装 AppTabBar

3.1 组件设计目标

企业级底导航需要满足:

  1. 图标+文字双显示
  2. 未选中态灰色、选中态主题色
  3. 响应深色模式颜色自动切换
  4. Badge 角标支持消息提醒
  5. 可配置Tab 数量与内容

3.2 TabConfig 数据模型

// model/TabConfig.ets export class TabConfig { name: string; // 'home' / 'chart' / 'budget' / 'user' label: string; // 显示文字 icon: Resource; // 图标资源 badge?: number = 0; // 角标数字,0 不显示 constructor(name: string, label: string, icon: Resource, badge?: number) { this.name = name; this.label = label; this.icon = icon; this.badge = badge ?? 0; } }

3.3 AppTabBar 组件实现

// components/navigation/AppTabBar.ets import { TabConfig } from '../../model/TabConfig'; import { AppColors } from '../../theme/Colors'; import { AppFontSize } from '../../theme/Typography'; import { AppSpace } from '../../theme/Spacing'; @Component export struct AppTabBar { @Prop tabs: TabConfig[] = []; @Prop currentIndex: number = 0; @BuilderParam onTabClick: (index: number) => void; build() { Row() { ForEach(this.tabs, (tab: TabConfig, index: number) => { Column() { Image(tab.icon) .width(24) .height(24) .fillColor(this.currentIndex === index ? AppColors.Budget : AppColors.SecondaryText) Text(tab.label) .fontSize(AppFontSize.XS) .fontColor(this.currentIndex === index ? AppColors.Budget : AppColors.SecondaryText) .margin({ top: AppSpace.XS }) // Badge 角标 if (tab.badge > 0) { Text(tab.badge.toString()) .fontSize(10) .fontColor('#FFFFFF') .backgroundColor(AppColors.Expense) .borderRadius(8) .padding(2) .position({ x: 24, y: -4 }) } } .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .onClick(() => { this.onTabClick(index); }) }, (tab: TabConfig) => tab.name) } .width('100%') .height(56) .backgroundColor(AppColors.CardBackground) .border({ width: { top: 1 }, color: AppColors.Separator }) } }

关键技术@BuilderParam用于将点击回调暴露给父组件,这是 ArkUI V1 组件通信的标准方案。


四、四个 Tab 页面占位

4.1 HomeView 占位

// pages/HomeView.ets @Entry @Component struct HomeView { build() { Column() { Text('首页') .fontSize(24) .fontWeight(FontWeight.Bold) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }

4.2 StatisticsView 占位

// pages/StatisticsView.ets @Entry @Component struct StatisticsView { build() { Column() { Text('统计分析') .fontSize(24) .fontWeight(FontWeight.Bold) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }

4.3 BudgetView 与 ProfileView

// pages/BudgetView.ets @Entry @Component struct BudgetView { build() { Column() { Text('预算中心') .fontSize(24) .fontWeight(FontWeight.Bold) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } } // pages/ProfileView.ets @Entry @Component struct ProfileView { build() { Column() { Text('我的') .fontSize(24) .fontWeight(FontWeight.Bold) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } }

五、主页面集成

5.1 MainView 集成 Tabs

// pages/MainView.ets import { AppTabBar } from '../components/navigation/AppTabBar'; import { TabConfig } from '../model/TabConfig'; import { HomeView } from './HomeView'; import { StatisticsView } from './StatisticsView'; import { BudgetView } from './BudgetView'; import { ProfileView } from './ProfileView'; @Entry @Component struct MainView { @State currentIndex: number = 0; @State tabs: TabConfig[] = [ new TabConfig('home', '首页', $r('app.media.icon_tab_home')), new TabConfig('chart', '统计', $r('app.media.icon_tab_chart')), new TabConfig('budget', '预算', $r('app.media.icon_tab_budget')), new TabConfig('user', '我的', $r('app.media.icon_tab_user')) ]; build() { Stack({ alignContent: Alignment.TopStart }) { // 内容区 Column() { if (this.currentIndex === 0) { HomeView() } else if (this.currentIndex === 1) { StatisticsView() } else if (this.currentIndex === 2) { BudgetView() } else { ProfileView() } } .width('100%') .height('100%') .padding({ bottom: 56 }) // 底导航 AppTabBar({ tabs: this.tabs, currentIndex: this.currentIndex, onTabClick: (index: number) => { this.currentIndex = index; } }) } .width('100%') .height('100%') } }

5.2 路由配置更新

// entry/src/main/resources/base/profile/main_pages.json { "src": [ "pages/MainView", // 主入口改为 MainView "pages/HomeView", "pages/StatisticsView", "pages/BudgetView", "pages/ProfileView" ] }
// EntryAbility.ets 入口改为 MainView windowStage.loadContent('pages/MainView', (err) => { ... });

架构调整:入口从 HomeView 改为 MainView,底导航由 MainView 持有,四个 Tab 页面作为子组件。这是鸿蒙多 Tab 应用的标准结构。


六、图标资源准备

6.1 Tab 图标规格

规格
尺寸24x24 dp
格式PNG(带 alpha 通道)或 SVG
色彩单色(通过 fillColor 染色)
命名icon_tab_{name}.png

6.2 资源放置

entry/src/main/resources/base/media/ ├─ icon_tab_home.png ├─ icon_tab_chart.png ├─ icon_tab_budget.png └─ icon_tab_user.png

统一染色方案:图标统一用白色 PNG,通过Image.fillColor()动态染色。这样未选中态、选中态、深色模式都能复用同一张图。


七、最佳实践

7.1 Tab 切换性能优化

// ❌ 错误:每次切换都重建所有 Tab 页面 Tabs() { TabContent() { HomeView() } TabContent() { StatisticsView() } // 切换到统计时,HomeView 仍在后台占用内存 } // ✅ 正解:用 if 懒加载 Stack() { if (this.currentIndex === 0) { HomeView() } else if (this.currentIndex === 1) { StatisticsView() } }

7.2 Tab 状态保持

如果希望 Tab 切换时保留各页面状态:

@State homeState: HomeState = new HomeState(); @State statsState: StatsState = new StatsState(); // 用 Stack 叇叠所有页面,通过 visibility 控制 Stack() { HomeView({ state: this.homeState }).visibility(this.currentIndex === 0 ? Visibility.Visible : Visibility.Hidden) StatisticsView({ state: this.statsState }).visibility(this.currentIndex === 1 ? Visibility.Visible : Visibility.Hidden) }

权衡:状态保持会占用更多内存,HarmonyLedger 暂不启用,后续 v0.2.7 性能优化篇再讨论。


八、运行验证

8.1 编译检查

hvigorw assembleHap--modemodule-pproduct=default

8.2 运行验证

  1. 应用启动后显示首页,底导航四个 Tab 均可见
  2. 点击"统计" Tab,切换到统计分析页
  3. 选中态 Tab 文字和图标变蓝色(AppColors.Budget)
  4. 未选中态 Tab 灰色,深色模式下同步切换


九、常见问题

9.1 TabContent 传参错误

错误原因解决
TabContent(builder)不渲染不能传 builder 对象TabContent() { ... }包裹内容
图标不显示资源路径错误检查$r('app.media.icon_tab_xxx')名是否一致

9.2 切换闪烁

// 原因:if 切换会销毁重建页面 // 解决:用 visibility 保留所有页面 .visibility(currentIndex === index ? Visibility.Visible : Visibility.Hidden)

9.3 底部遮挡

// 内容区需预留底导航高度 Column() { ... } .padding({ bottom: 56 }) // 与 AppTabBar.height 一致

十、Git 提交

10.1 Commit Message

gitadd.gitcommit-m"feat(navigation): 实现底部 Tab 导航与 AppTabBar 封装 - 新增 AppTabBar 通用底导航组件 - 新增 TabConfig 数据模型 - 创建 HomeView/StatisticsView/BudgetView/ProfileView 占位页 - 集成 MainView 持有 Tabs 切换 - 更新 EntryAbility 入口为 MainView - 添加四个 Tab 图标资源"

10.2 CHANGELOG

## [v0.0.4] - 2026-07-27 ### Added - components/navigation/AppTabBar.ets:通用底导航组件 - model/TabConfig.ets:Tab 配置数据模型 - pages/MainView.ets:主页面持有 Tabs - pages/StatisticsView.ets、BudgetView.ets、ProfileView.ets:占位页 - 资源:icon_tab_home/chart/budget/user.png ### Changed - EntryAbility 入口改为 MainView - main_pages.json 增加 MainView 路由

附录:运行效果截图

上图展示了本模块的完整运行效果。实际项目中可使用 DevEco Studio 模拟器或真机截图替换此占位图。

总结

本文完整介绍了 HarmonyLedger底部 Tab 导航的实现,包含原生 Tabs 用法、AppTabBar 组件封装、四个 Tab 页面占位、主页面集成。通过本篇你可以:

  • 使用 ArkUI Tabs 组件搭建底导航
  • 封装可复用的 AppTabBar 组件支持图标+文字+角标
  • 实现主题色响应选中态与深色模式
  • 理解 EntryAbility 与 MainView 的入口关系

下一篇预告:《首页布局与 SummaryCard 设计》将开发首页核心 UI:今日支出、今日收入、最近账单列表,并封装 SummaryCard 汇总卡片组件。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

  • 本篇源码:GitHub Tag v0.0.4
  • ArkUI Tabs 文档:tabs
  • ArkUI Image 组件:image
  • TabContent API:tabcontent
  • ArkUI 组件通信:component-communication
  • BarPosition 枚举:barposition
  • 鸿蒙图标资源规范:icon-resources
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 2:21:27

PIC24与UG95实现全球物联网通信方案

1. 项目背景与核心目标在物联网和远程监控领域,设备的地理位置限制一直是开发者面临的重大挑战。传统解决方案往往依赖固定网络基础设施或特定运营商的覆盖范围,这严重制约了设备部署的灵活性。而UG95模块与PIC24HJ256GP610微控制器的组合,为…

作者头像 李华
网站建设 2026/7/29 2:19:22

HarmonyOS NEXT 企业级记账APP:首页布局与 SummaryCard 设计

首页布局与 SummaryCard 设计 本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 05 篇,对应 Git Tag v0.0.5。承接第 04 篇的底导航,本篇开发首页核心 UI:今日支出、今日收入、最近账单列表,并封…

作者头像 李华
网站建设 2026/7/29 2:19:19

Allure与Jenkins Pipeline集成:打造一体化测试报告与质量仪表盘

1. 项目概述:为什么我们需要一体化的测试报告在持续集成和持续交付的实践中,自动化测试是保障软件质量的基石。但很多时候,我们面临的困境是:测试跑完了,结果也“通过”了,可我们得到的只是一堆冰冷的日志文…

作者头像 李华
网站建设 2026/7/29 2:17:56

Unity3D动态场景节点管理:架构设计与性能优化实战

1. 项目概述:为什么我们需要动态管理场景节点?在Unity3D项目开发中,尤其是涉及大地图、开放世界、关卡编辑器或者需要运行时动态加载大量内容的游戏时,我们经常会遇到一个核心挑战:如何高效、有序地管理场景中成千上万…

作者头像 李华
网站建设 2026/7/29 2:14:29

Cura图片转3D模型全攻略:从原理到实践,轻松制作个性化浮雕

1. 项目概述:当3D打印遇上平面图片如果你手头有一台3D打印机,脑子里冒出一个想法:能不能把一张普通的JPG照片,比如一张风景照、一个Logo,甚至是一个签名,直接变成可以触摸的立体浮雕打印出来?答…

作者头像 李华
网站建设 2026/7/29 2:07:34

大模型AI-Agent 上下文管理

部分内容可能来自网络或者由AI生成。 如有雷同,纯属巧合,仅供学习参考之用。Agent 上下文管理:从窗口约束到工程化治理决定每一轮交互时,模型的上下文窗口里应该出现哪些信息、以什么结构出现、何时移除。它是介于"提示词工程…

作者头像 李华