HarmonyOS7 Tabs 页面切换怎么写才不乱:父页面别管太多

文章目录
前言
Tabs 做页面切换很简单,真正容易乱的是状态边界。
我见过不少频道页,父页面同时管理当前下标、每个频道的列表、刷新状态、空态、错误态,最后一个页面变成几百行。刚开始还能改,等产品说要加“本地”“关注”“热榜”,就开始牵一发动全身。
Tabs 页面要稳,父级只管频道配置和当前索引,频道内容自己处理自己的业务。
这篇用 HarmonyOS7 写一个资讯频道页,重点看 Tabs、TabContent、@Builder 和 @Prop 怎么配合。
为什么 Tabs 页面特别容易越写越乱
因为它表面上只是“切换一下页面”,实际背后会牵出一堆状态:当前下标、每个频道的数据、加载进度、空态、错误态,甚至滚动位置。很多人一开始图省事,全塞进父页面,等频道一多,整个页面就会开始打结。

更麻烦的是,这种乱不是立刻爆出来的。通常是产品多加两个频道,或者某个频道改成单独请求接口时,你才发现状态早就缠在一起了。
所以写 Tabs 时,重点不是先把切换效果做出来,而是先划清“谁该管什么”。这一步做对了,后面加功能会轻松很多。
先划清父子职责
频道页我会先按职责拆,不急着写 UI。
| 角色 | 应该负责 | 不应该负责 |
|---|---|---|
| 父页面 | 频道配置、当前选中下标 | 每个频道的加载细节 |
TabContent |
承载某个频道内容 | 修改其他频道状态 |
| 自定义 tab 样式 | 标题、高亮、下划线 | 拉接口、处理列表数据 |

这个边界划清楚,后面新增频道时,通常只是改 channels 配置和内容组件,不会把旧逻辑翻出来重写。
Tabs的重点不是能不能切换,而是切换以后状态归谁管。
先把父页面和频道页的边界想清楚
写 Tabs 时,我会先问一个很实际的问题:父页面到底只是负责切换,还是还要顺手托管每个频道的数据、加载和错误状态。如果这个边界一开始就含糊,后面最常见的结果就是父页面越长越大,频道页反而只剩展示壳子。
更稳的做法是把父页面当成“频道容器”:它负责频道配置、当前索引、tab 样式这些全局结构;每个频道内容自己处理自己的列表、空态、错误态和刷新。这样以后新增频道、改单个频道接口,影响面都会小很多。
对小白来说,这一段不是抽象原则,而是非常具体的写法判断:凡是只属于某个频道的问题,尽量别往父页面堆。
完整 ArkUI 示例
下面的例子包含一个父页面和一个频道内容组件。父页面维护频道配置,NewsListPanel 只关心自己要展示哪个频道。
interface NewsChannel {
id: number
name: string
count: number
}
@Component
struct NewsListPanel {
@Prop channelName: string
@Prop count: number
build() {
List({ space: 10 }) {
ForEach([0, 1, 2], (index: number) => {
ListItem() {
Column({ space: 6 }) {
Text(`${this.channelName}频道文章 ${index + 1}`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text('这里放摘要、作者和发布时间,真实项目里可以替换为接口数据。')
.fontSize(12)
.fontColor('#666666')
.maxLines(2)
}
.alignItems(HorizontalAlign.Start)
.padding(14)
.backgroundColor(Color.White)
.borderRadius(10)
}
}, (index: number) => `${this.channelName}-${index}`)
}
.padding({ left: 12, right: 12, top: 12 })
}
}
@Entry
@Component
struct NewsTabsPage {
@State currentIndex: number = 0
private channels: NewsChannel[] = [
{ id: 1, name: '推荐', count: 18 },
{ id: 2, name: '关注', count: 6 },
{ id: 3, name: '本地', count: 4 },
{ id: 4, name: '科技', count: 12 }
]
@Builder
TabLabel(item: NewsChannel, index: number) {
Column({ space: 4 }) {
Text(item.name)
.fontSize(15)
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Regular)
.fontColor(this.currentIndex === index ? '#1E88E5' : '#555555')
if (this.currentIndex === index) {
Rect().width(18).height(3).fill('#1E88E5').radius(2)
}
}
.height(44)
.justifyContent(FlexAlign.Center)
}
build() {
Column() {
Tabs({ index: this.currentIndex }) {
ForEach(this.channels, (item: NewsChannel, index: number) => {
TabContent() {
NewsListPanel({ channelName: item.name, count: item.count })
}
.tabBar(this.TabLabel(item, index))
}, (item: NewsChannel) => item.id.toString())
}
.barMode(BarMode.Scrollable)
.onChange((index: number) => {
this.currentIndex = index
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F7FA')
}
}
把关键代码一段段拆开
channels 是整个 tab 的配置来源。标题、key、内容参数都从这里来,避免同一份频道信息散落在多个地方。
currentIndex 只表示当前选中的频道下标。它不应该顺手承担“当前频道是否加载中”“当前频道有没有数据”这些职责。那些状态应该放进频道内容组件或对应的数据模型里。
TabLabel 用 @Builder 收起自定义 tab 样式。这样 TabContent 里不会塞一大段重复 UI,后面改高亮颜色、下划线宽度也只改一个地方。
NewsListPanel 使用 @Prop 接收频道参数,说明它只消费父级传来的值,不反向修改父页面。这个方向很重要,父子组件互相改状态,页面很快就会乱。
容易踩坑的地方
频道数量超过四个时,建议使用 barMode(BarMode.Scrollable)。如果强行平均分,标题会被压窄,中文频道名很容易挤在一起。
不要给每个频道单独写一份 TabContent。比如“推荐”写一段,“关注”复制一段,“本地”再复制一段。短期快,长期一定难维护。频道结构一致时,用数组渲染更合适。
还有一个小细节:如果每个频道都要请求接口,别在父页面用一个 loading 管所有频道。否则一个频道加载中,其他频道也像被锁住了。
新手最容易踩的坑
第一个坑,是父页面管得太多。下标、频道列表、加载状态、错误状态全堆在一起,代码一开始看着直接,后面改一个频道就开始连锁反应。
第二个坑,是每个频道都复制一份 TabContent。短期看着省事,长期一定会变成“每个页面改一点,四五处一起改”的局面。
第三个坑,是把频道内容也放到父页面里统一处理。这样一来,任何一个频道出问题,父页面都得一起跟着重写。
放进真实项目还要补什么
真实项目里,频道内容通常会接接口、做分页、处理空态和错误态,所以 TabContent 更适合自己管自己的数据。父页面只保留频道配置和当前索引,就够了。这样某个频道请求慢、接口挂了,影响的也只是它自己,不会把整个 tab 容器都拖下水。
如果后面要加埋点、刷新联动或者频道排序,先把频道内容拆成独立组件会更省力。尤其是“切回上一个频道要不要保留滚动位置”“切换频道时要不要自动刷新”这类需求,都会直接考验你前面职责边界划得清不清楚。
还有一个常见扩展点是频道动态配置。比如接口返回哪些频道可见、某些频道需要灰度发布。到了这一步,如果父页面只维护频道配置数组,改动会很轻;如果父页面早就和每个频道业务缠在一起,后面改起来就会非常痛苦。
写在最后
Tabs 本身不复杂,复杂的是职责边界。谁管切换,谁管数据,谁管状态,先分清楚,再写代码,页面就不容易乱。
更多推荐



所有评论(0)