【中国方言题库|14】HarmonyOS ArkTS 主导航实战:统一页面入口、返回路径和参数校验

HarmonyOS 应用中的“导航”通常不止一种:底部 Tab 是同一主容器里的内容切换,搜索、题库详情、练习和设置则是路由栈中的页面跳转。若把两者混为一谈,常见结果是点击“更多”不断向路由栈压入重复主页、设置页返回后落错 Tab、启动页出现在返回历史里,或者目标页在参数缺失时直接使用空 ID。
中国方言题库把五个一级入口集中到 Index,由 currentTabIndex 切换首页、题库、报考、收藏和“我的”;需要独立返回路径的二级页面才使用 router.pushUrl()。HomePage 既能切换主 Tab,也能携带题型参数进入搜索页。本文面向 HarmonyOS 5.0 及以上版本,基于真实 ArkTS 源码复核主导航、路由返回与参数读取,并特别说明当前参数校验并不完全一致的事实。
本文唯一核验标记:一级入口改状态,二级页面才进入路由栈。
一、导航设计先区分两类目标
当前项目存在两种导航语义:
一级入口:
首页 / 题库 / 报考 / 收藏 / 我的
二级页面:
搜索 / 分类 / 题库详情 / 练习 / 考试结果 / 学习统计 / 设置
一级入口共享同一个 Index 页面容器,切换时不需要创建新的页面路由。二级页面拥有独立标题、返回按钮或业务参数,适合进入路由栈。
二、本文依据的真实源码
核心文件包括:
entry/src/main/ets/pages/Index.ets
entry/src/main/ets/views/HomePage.ets
entry/src/main/ets/common/components/TopBar.ets
entry/src/main/ets/common/components/BankCard.ets
entry/src/main/ets/pages/BankDetailPage.ets
entry/src/main/ets/pages/SearchPage.ets
entry/src/main/ets/pages/PracticePage.ets
entry/src/main/ets/pages/ExamResultPage.ets
entry/src/main/ets/pages/SettingsPage.ets
entry/src/main/resources/base/profile/main_pages.json
文章讨论的是当前基于 @kit.ArkUI 中 router 的实际实现,不把 Navigation、NavPathStack 或跨 Ability 导航写成现有能力。
三、Index 是五个一级入口的唯一装配点
Index 直接引入五个 Tab 组件:
import { HomePage } from '../views/HomePage'
import { BankListPage } from '../views/BankListPage'
import { ExamTab } from '../views/ExamTab'
import { FavoritePage } from '../views/FavoritePage'
import { MinePage } from '../views/MinePage'
它不承载具体学习业务,只负责决定当前显示哪一个一级页面、绘制导航项以及处理安全边距。一级入口的“唯一装配点”让 HomePage 和 MinePage 不必各自创建一套根路由。
四、Tab 配置集中维护标题和图标
导航元数据使用统一接口:
interface TabItem {
title: string
iconNormal: Resource
iconSelected: Resource
}
五个入口都存放在 tabs 数组中:
private tabs: TabItem[] = [
{ title: '首页', iconNormal: ..., iconSelected: ... },
{ title: '题库', iconNormal: ..., iconSelected: ... },
{ title: '报考', iconNormal: ..., iconSelected: ... },
{ title: '收藏', iconNormal: ..., iconSelected: ... },
{ title: '我的', iconNormal: ..., iconSelected: ... }
]
标题、默认图标和选中图标不再散落到每个页面里。增加一级入口仍需同步内容分支和导航构建,但现有五项的视觉配置已经集中。
五、currentTabIndex 是一级导航的共享状态
Index 通过:
@StorageLink('currentTabIndex') currentIndex: number = 0
读取应用级 Tab 状态。EntryAbility.onCreate() 会预先创建该键,首页、“我的”和设置页也通过 @StorageLink 修改同一个值。
这使业务组件可以发出“切换到题库”或“切换到收藏”的意图,而不必拿到 Index 实例,也不必把回调从根组件层层传递。
六、PageContent 如何把索引映射到页面
页面内容由一个 @Builder 选择:
@Builder
PageContent() {
if (this.currentIndex === 0) {
HomePage()
} else if (this.currentIndex === 1) {
BankListPage()
} else if (this.currentIndex === 2) {
ExamTab()
} else if (this.currentIndex === 3) {
FavoritePage()
} else {
MinePage()
}
}
索引 0 至 3 对应明确页面,其他所有值落到 MinePage。这个 else 提供了渲染兜底,却不等于完成严格索引校验:如果误写 currentTabIndex = 99,界面会显示“我的”,但导航项没有任何一个满足 currentIndex === index。
七、点击 Tab 只改状态,不压入路由栈
底部导航项的核心行为是:
.onClick(() => {
this.currentIndex = index
})
侧边导航项也使用相同赋值。切换首页、题库、报考、收藏和“我的”时,路由栈没有新增页面;系统返回不会逐个回放用户切换过的 Tab。
这正是一级入口与二级页面应当区别处理的原因。
八、选中反馈由同一个索引驱动
图标、文字颜色和字重都基于 currentIndex:
Image(
this.currentIndex === index
? this.tabs[index].iconSelected
: this.tabs[index].iconNormal
)
Text(this.tabs[index].title)
.fontColor(
this.currentIndex === index
? Colors.PRIMARY
: Colors.TEXT_HINT
)
点击事件和视觉选中态读取同一数据源,不需要再维护一个 selectedTab。状态更新后,内容和导航反馈一起变化。
九、错题徽标为什么挂在收藏入口
Index 还读取:
@StorageLink('wrongRecords') wrongRecords: WrongRecord[] = []
当收藏 Tab 的索引为 3 且错题数大于 0 时显示徽标,超过 99 显示 99+。底部导航和侧边导航复用相同规则,只调整字号、圆角和位置。
徽标数量直接来自当前错题集合长度,没有单独维护一个手工计数器。
十、首页的“更多”为什么不使用 pushUrl
推荐题库区域的“更多”回调是:
onAction: () => {
this.currentTabIndex = 1
}
继续练习卡片也执行同样赋值。目标本来就是主容器中的题库 Tab,因此直接改一级状态比 router.pushUrl({ url: 'pages/Index' }) 更合适,后者会重复创建根页面并让返回栈变复杂。
十一、首页快捷入口可以一次切换两个状态
“错题复习”按钮执行:
this.favoriteTabIndex = 2
this.currentTabIndex = 3
先指定收藏页内部的错题子 Tab,再切换到收藏一级入口。两个状态都由 EntryAbility 提前初始化,并通过 AppStorage 共享。
这条链路没有路由参数,也没有新页面入栈,适合主容器内部的组合导航。
十二、报考快捷入口同样是一级切换
首页“真实题量计时测试”调用:
this.currentTabIndex = 2
它只是打开报考 Tab,不直接启动某个题库考试。真正开始考试时,ExamTab 才通过 router.pushUrl() 进入 PracticePage 并携带 bankId 与 mode: 'exam'。
把“选择入口”和“执行具体考试”分成两步,参数来源更清楚。
十三、二级页面通过 pushUrl 建立返回路径
首页搜索按钮使用:
router.pushUrl({
url: 'pages/SearchPage'
})
分类入口使用:
router.pushUrl({
url: 'pages/CategoryPage'
})
题库卡片、学习统计和设置也使用 pushUrl()。这些页面需要用户完成操作后返回先前位置,因此进入路由栈是合理行为。

十四、TopBar 统一二级页面返回动作
通用顶部栏把返回按钮行为集中为:
.onClick(() => {
router.back()
})
它还统一返回图标、46vp 触控容器、标题单行省略和顶部安全区。题库详情、考试结果、设置等页面不必各自重写返回按钮。
当前 TopBar 没有自定义返回拦截、未保存确认或栈为空兜底,使用者应在有正常来源页面的二级场景中调用。
十五、启动页为何使用 replaceUrl
SplashPage 在计时结束后调用:
router.replaceUrl({
url: 'pages/Index'
})
启动页不是业务返回目标,因此用 replaceUrl() 替换当前路由。用户进入主导航后按返回,不会再次回到启动动画。
这和主页面进入搜索页时使用 pushUrl() 的语义不同:一个是替换临时入口,一个是建立可返回的业务页面。
十六、考试流程为何也会使用 replaceUrl
练习页完成考试后,会使用 replaceUrl() 进入 ExamResultPage;结果页选择再次考试时,也可用 replaceUrl() 返回新的考试页面。
目的是避免“答题页 -> 结果页 -> 同一答题页”不断叠加历史。当前代码仍需结合实际返回路径测试,确认用户按返回时落点符合产品预期。
十七、页面清单是路由字符串的静态边界
main_pages.json 注册:
{
"src": [
"pages/SplashPage",
"pages/Index",
"pages/BankDetailPage",
"pages/PracticePage",
"pages/ExamResultPage",
"pages/SearchPage",
"pages/CategoryPage",
"pages/LearningStatsPage",
"pages/SettingsPage"
]
}
调用中的 URL 必须与清单一致。当前项目仍使用字符串路径,没有统一的路由常量或编译期路径检查,因此新增页面时要同时核验注册与调用。
十八、题型入口如何携带可选参数
首页的题型卡片跳转:
router.pushUrl({
url: 'pages/SearchPage',
params: {
categoryType: cat.type,
categoryName: cat.name
}
})
SearchPage 定义:
interface SearchParams {
categoryType?: string
categoryName?: string
}
两个字段都是可选,因为搜索页也允许从顶部搜索按钮无参数进入。
十九、SearchPage 的参数读取有明确守卫
页面出现时执行:
const params =
router.getParams() as SearchParams | undefined
if (params && params.categoryType) {
this.categoryType = params.categoryType
this.categoryName =
params.categoryName || params.categoryType
this.keyword = this.categoryName
this.doSearch()
}
无参数时保持普通搜索首页;有 categoryType 时才自动执行分类搜索;categoryName 缺失则回退为类型字符串。这个守卫与参数的可选语义一致。
二十、BankDetailPage 如何验证 bankId
题库详情页定义:
interface BankDetailParams {
bankId: string
}
读取时不仅检查 params,还检查 params.bankId:
if (params && params.bankId) {
this.bank = getBankById(params.bankId)
}
getBankById() 可能返回 undefined,页面据此显示“未找到题库”空态。也就是说,缺失 ID 和不存在的 ID 都不会直接解引用空对象。
二十一、专属详情页如何绕过路由参数
BankDetailContent 还支持 fixedBankId。若专属地区页面传入固定 ID:
if (this.fixedBankId.length > 0) {
this.bank = getBankById(this.fixedBankId)
return
}
它优先使用组件属性,不再读取路由参数。通用详情页和专属页面复用同一内容组件,同时保留两种可靠的数据来源。
二十二、BankCard 保持路由目标与参数同源
卡片根据 bank.id 决定专属详情页或通用详情页,并始终携带:
params: {
bankId: this.bank.id
}
显示名称、封面、路由目标和参数都来自同一个 Bank 对象,减少手工拼装时“页面是粤语、参数却是四川话”的错配。
二十三、PracticePage 的参数契约更复杂
练习页接口为:
interface PracticeParams {
bankId: string
chapterId?: string
mode: string
records?: string
}
它支持章节练习、随机练习、模拟考试、错题练习和错题分析。bankId 与 mode 在类型上必填,章节 ID 和序列化答题记录可选。
复杂参数越多,越需要在目标页按运行时数据验证,而不能只依赖 TypeScript/ArkTS 的类型断言。
二十四、PracticePage 当前只验证 params 是否存在
真实读取逻辑以:
if (params) {
this.bankId = params.bankId
this.mode = params.mode || 'chapter'
this.chapterId = params.chapterId || ''
}
开头。它给 mode 和 chapterId 提供了回退,但没有显式验证 bankId 非空,也没有把 mode 限制在允许集合中。
因此不能说当前参数校验已经完整。调用侧都传递了预期值,但目标页的运行时边界仍可加强。
二十五、错题分析参数的 JSON 解析有容错
当 mode === 'wrongAnalysis' 时:
if (params.records) {
try {
examRecords =
JSON.parse(params.records) as AnswerRecord[]
} catch (_) {
examRecords = []
}
}
非法 JSON 会回退为空数组,后续只从成功匹配到的 questionId 生成题目。这里处理了格式异常,但没有逐项验证解析对象是否真的符合 AnswerRecord。
二十六、空题目时当前怎样回退
练习页根据章节、错题或题库加载题目后,如果结果为空且不是错题分析,会再次调用:
this.questions = getQuestions(params.bankId)
这是从细分来源回退到整个题库的策略。若 bankId 本身无效,回退仍可能为空,因此 UI 还需要正确处理空题集;参数回退不等于保证一定有题。
二十七、ExamResultPage 的参数风险更值得注意
结果页先把所有字段初始化为零或空字符串,然后:
const params =
router.getParams() as ExamResultParams | undefined
if (params) {
this.bankId = params.bankId
this.score = params.score
// ...
}
之后无论参数是否存在,都会计算等级并调用 addExamHistory()。如果页面被无参数直接打开,当前实现可能写入一条空题库、零分的考试历史。
这不是“已经完成严格参数校验”,而是现有导航链路依赖调用侧正确传参的明确风险点。
二十八、设置页返回主 Tab 的处理很有代表性
设置页从“我的”通过 pushUrl() 打开。用户在设置页选择收藏子页或主 Tab 时,先修改共享状态,再返回:
private openFavoriteTab(tabIndex: number): void {
this.favoriteTabIndex = tabIndex
this.currentTabIndex = 3
router.back()
}
private openMainTab(tabIndex: number): void {
this.currentTabIndex = tabIndex
router.back()
}
router.back() 恢复原来的 Index,共享状态决定它显示哪个 Tab。这比从设置页再次 pushUrl('pages/Index') 更能保持根页面唯一。
二十九、“我的”快捷入口同样不创建新根页面
“我的”中的错题本、笔记和考试记录直接修改 favoriteTabIndex 或 currentTabIndex。学习统计和设置才进入二级页面。
同一个组件根据目标语义选择“改状态”还是“压路由”,体现了主导航边界:
目标是一级 Tab -> 改 currentTabIndex
目标是独立业务页 -> router.pushUrl
三十、主导航的安全区与返回路径相互独立
Index 读取顶部和底部避让区,计算导航高度;二级页面的 TopBar 读取顶部避让区,页面底部工具栏读取导航指示区。
安全区解决“内容是否可达”,路由解决“页面如何进入和返回”。两者都属于导航体验,但代码职责不应混在同一个路由函数里。
三十一、当前侧边导航分支实际上不可达
Index 的条件是:
if (
this.currentBp === 'sm' ||
this.currentBp === 'md' ||
this.currentBp === 'lg'
) {
// 底部导航
} else {
// 侧边导航
}
而当前 BreakpointSystem 只会写入 sm、md、lg。因此注释中的平板/折叠屏侧边栏在现有断点契约下不会被选中。
文章不能把未到达的分支包装成已验证的多设备导航能力。若要启用侧栏,应调整条件或扩展断点类型,并做真实窗口测试。
三十二、索引参数也需要边界校验
BottomNavItem(index) 和 SideNavItem(index) 会访问 this.tabs[index]。当前调用只传入 0 至 4,所以正常安全。
SettingsPage.openMainTab(tabIndex) 和 openFavoriteTab(tabIndex) 接收普通 number,方法内部没有限制范围。当前按钮调用值来自固定代码,但若未来改为外部参数,应先校验有效区间。
三十三、路由字符串适合集中成常量吗
当前 URL 分散在首页、卡片、考试、收藏、“我的”和设置等文件中。规模尚可,但相同路径如 pages/SearchPage、pages/PracticePage 已出现多次。
后续可以定义类型明确的路由常量和参数接口,减少拼写错误。仍要保持简单:常量只集中路径,不必为十几个本地页面引入复杂导航框架。
三十四、参数校验可以怎样补齐
以练习页为例,可以先做最小运行时守卫:
type PracticeMode =
| 'chapter'
| 'random'
| 'exam'
| 'wrong'
| 'wrongAnalysis'
private isPracticeMode(value: string): boolean {
return value === 'chapter' ||
value === 'random' ||
value === 'exam' ||
value === 'wrong' ||
value === 'wrongAnalysis'
}
然后验证 bankId、模式和可选 JSON。结果页在参数缺失时应显示错误态或返回,而不是保存默认历史。这里是基于现状的改进建议,不代表源码已改。
三十五、导航结构的四层职责

可以把当前导航拆为四层:
Root State -> currentTabIndex / favoriteTabIndex
Root View -> Index / BottomNavItem / PageContent
Route -> pushUrl / replaceUrl / back
Target Page -> getParams / 默认值 / 空态 /业务加载
根状态不负责解析 bankId,目标页不负责创建底部 Tab,路由 API 不负责修复非法业务参数。每层只处理自己的边界。
三十六、推荐的主导航验证矩阵
至少覆盖:
1. 冷启动后默认显示首页,首页 Tab 有选中态
2. 依次切换五个 Tab,不增加可见路由历史
3. 首页“更多”和“继续练习”都切到题库 Tab
4. 首页考试入口切到报考 Tab
5. 错题复习同时切到收藏 Tab 与错题子页
6. 搜索按钮 push 到 SearchPage,返回恢复原 Tab
7. 分类卡片携带 categoryType/categoryName 并自动搜索
8. 题库卡片携带 bankId,非法 ID 显示“未找到题库”
9. 练习页分别验证 chapter/random/exam/wrong 模式
10. 错题分析 records 非法 JSON 时不崩溃
11. 结果页正常参数只保存一次正确考试历史
12. 设置页切换目标 Tab 后 back 回到同一个 Index
13. 启动页进入主页后,返回不再显示 SplashPage
14. 系统返回与顶部返回按钮行为一致
15. 旋转和窗口缩放后导航不遮挡内容
还应补一项负向测试:开发环境直接无参数打开 ExamResultPage,确认当前风险并在后续修复后验证不会写入无效历史。
三十七、当前实现真正统一了什么
它统一了以下可复核规则:
五个一级入口只由 Index 装配
一级切换只修改 AppStorage 状态
二级页面才使用 pushUrl
启动页用 replaceUrl 退出返回栈
二级顶部栏统一 router.back
题型搜索参数允许缺省
题库详情对缺失或非法 bankId 显示空态
设置页通过“改状态 + back”回到唯一根页面
同时,练习模式枚举、结果页必填参数和 Tab 索引仍缺少完整运行时校验,这些边界必须原样记录。
三十八、结语
中国方言题库的主导航没有把所有点击都变成页面跳转,而是先判断目标属于根容器还是独立业务页:一级入口修改 currentTabIndex,二级页面通过路由栈进入;replaceUrl() 处理不应返回的启动页和流程节点,router.back() 恢复已有根页面;目标页再按各自契约读取参数。
这套结构已经减少重复根页面和混乱返回路径,也通过可选参数、空态和 JSON 容错覆盖部分异常。但参数校验并非全链路完成,尤其 PracticePage 的必填字段和 ExamResultPage 的无参数写入风险仍需正视。只有同时讲清已实现能力与未完成边界,导航设计才真正可复核。
AI 辅助声明:本文由 AI 辅助整理与润色,Tab 状态、路由路径、返回行为、参数接口、守卫逻辑与现存风险均依据项目真实源码复核。
更多推荐




所有评论(0)