主导航工程封面

一个 HarmonyOS 应用页面不多时,导航代码通常从几行 router.pushUrl() 开始。等首页有五个主 Tab、搜索、分类、题库详情、练习、考试结果、设置等页面后,问题会迅速变复杂:有的入口应该切 Tab,有的入口应该压入路由栈;同一个题库可能进入专属详情页,也可能进入通用详情页;设置页返回前还要改变主页 Tab;分类搜索带可选参数,练习页又依赖 bankIdmode。如果这些语义没有分层,返回路径、参数缺失和重复页面都会一起出现。

本文基于句匠项目的真实 ArkTS 源码展开,主要核对 entry/src/main/ets/pages/Index.etsentry/src/main/ets/views/HomePage.ets,并用 SearchPage.etsPracticePage.etsSettingsPage.etsBankCard.etsTopBar.ets 验证路由发送、接收和返回行为。文章先还原源码已有的导航模型,再给出不改变现有页面结构的收口方法。

本文唯一源码标识:com.jiaweikang.one18

源码可证明的是:五个主页面使用 AppStoragecurrentTabIndex 切换;二级页面使用 router.pushUrl();Splash 到主页和答题到结果等替换场景使用 replaceUrl();通用顶部栏使用 router.back();搜索、题库详情、练习与考试结果页面通过 router.getParams() 接收参数并做不同程度的兜底。项目当前没有统一 NavigationService、路由常量表、运行时参数 Schema、深链或跨 Ability 导航,本文不会把建议写成已经实现的能力。

一、先区分两类导航:切 Tab 与进路由栈

句匠的主页 Index 里有首页、题库、错题本、收藏夹、我的五个主入口。它们不是五个独立路由,而是同一个 Index 页面内部的五个内容组件:

@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()
  }
}

当前索引来自:

@StorageLink('currentTabIndex') currentIndex: number = 0

点击底部导航项只修改索引:

.onClick(() => {
  this.currentIndex = index
})

这类操作没有新增路由记录。用户从首页切到题库,再切到收藏夹,仍停留在 Index 这个页面上。系统返回不会按“收藏夹→题库→首页”的顺序逐个回退,因为这些切换不是路由栈历史。

另一类是从主页进入搜索、分类、详情、设置等二级页。首页使用:

router.pushUrl({ url: 'pages/SearchPage' })

pushUrl 会把目标页放到当前路由栈顶部,二级页使用 router.back() 返回到之前的 Index。这两类导航必须分开,否则主 Tab 每切一次都压栈,用户按返回键会在一级页面间反复回退;反过来,如果所有二级页也只改全局状态,就失去了清晰的页面层级和返回路径。

主导航决策流程

用户意图句匠源码做法是否新增路由记录返回行为
首页切到题库currentTabIndex = 1仍由 Index 承担
首页切到错题本currentTabIndex = 2仍由 Index 承担
打开搜索页router.pushUrl()router.back() 返回首页
打开设置页router.pushUrl()返回前可修改主 Tab
Splash 进入主页router.replaceUrl()替换当前记录不返回 Splash
答题进入考试结果router.replaceUrl()替换答题页避免回到已提交状态

导航稳定的第一条规则,就是先判断用户是在同层切换,还是进入新的页面层级。

二、Index 是主导航唯一状态源

Index.ets 同时绘制内容区和导航项,图标、文字、页面内容都由同一个 currentIndex 派生:

@Builder
BottomNavItem(index: number) {
  Stack() {
    Column({ space: 3 }) {
      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
        )
        .fontWeight(
          this.currentIndex === index
            ? FontWeight.Bold
            : FontWeight.Normal
        )
    }
  }
  .onClick(() => {
    this.currentIndex = index
  })
}

内容区的 PageContent()、底部导航图标和文字样式都读取同一个索引,没有单独维护“选中的图标”“当前页面名称”等冗余状态。这样点击一个 Tab 只发生一次状态写入,ArkUI 重建时三处表现自然一致。

HomePage 也通过同一个键取得链接:

@StorageLink('currentTabIndex') currentTabIndex: number = 0

首页“开始纠错”卡片直接把索引改为题库:

.onClick(() => {
  this.currentTabIndex = 1
})

“限时挑战”切到索引 2,“错题本”则先指定收藏页内部子索引,再切到索引 3

this.ActionCard('错题本', `${this.wrongRecords.length} 道错题`, () => {
  this.favoriteTabIndex = 2
  this.currentTabIndex = 3
})

这里体现了主导航的层次:currentTabIndex 决定 Index 的一级内容,favoriteTabIndex 决定收藏夹内部要显示的子分区。两个状态都来自 AppStorage,但职责不同,不能合并成一个难以解释的数字。

当前源码中的一级索引含义是稳定的:

索引页面组件首页入口示例
0HomePage默认主页
1BankListPage推荐题库“更多”、开始纠错
2ExamTab限时挑战、考试记录
3FavoritePage错题本、收藏相关入口
4MinePage我的

如果后续增删 Tab,应同时更新 tabsPageContent() 和所有跨组件索引写入点。更稳的维护方式是用枚举或常量替代散落的 1/2/3,但文章不能声称当前源码已经完成这项封装。

三、HomePage 把入口按意图分成三种

首页上的入口看起来都是可点击卡片,实际承担三种不同导航语义。

第一种是一级 Tab 切换,例如“推荐题库”的“更多”:

SectionHeader({
  title: '推荐题库',
  actionText: '更多 >',
  onAction: () => {
    this.currentTabIndex = 1
  }
})

第二种是无参数二级页,例如搜索入口、分类总览、每日打卡、段位、排行榜和 AI 纠错:

router.pushUrl({ url: 'pages/SearchPage' })
router.pushUrl({ url: 'pages/CategoryPage' })
router.pushUrl({ url: 'pages/DailyCheckInPage' })
router.pushUrl({ url: 'pages/LevelProgressPage' })
router.pushUrl({ url: 'pages/LeaderboardPage' })
router.pushUrl({ url: 'pages/AICorrectPage' })

第三种是带参数二级页。点击题型分类时,首页把类型和值一起交给搜索页:

router.pushUrl({
  url: 'pages/SearchPage',
  params: {
    categoryType: cat.type,
    categoryName: cat.name
  }
})

三种入口不能只看组件外观。相同的 FeatureCard 可以打开一个路由页,相同的 ActionCard 也可以只切换 Tab。句匠通过回调把动作传给复用组件:

@Builder
FeatureCard(
  icon: Resource,
  title: string,
  sub: string,
  onTap: () => void
) {
  Row({ space: 10 }) {
    Image(icon)
    Column() {
      Text(title)
      Text(sub)
    }
  }
  .onClick(() => {
    onTap()
  })
}

复用组件只负责触发动作,不理解路由路径或主 Tab 索引。导航意图仍由调用处决定,这比让卡片组件根据标题字符串猜测目标页更可靠。

四、主 Tab 切换为什么不该使用 pushUrl

假设把五个主页面都注册为路由,然后底部按钮统一调用 router.pushUrl(),表面上代码很整齐,实际会产生三类问题。

首先是返回栈膨胀。用户连续点击“首页→题库→收藏夹→我的”,路由栈会留下多个一级页面。按系统返回时,不是退出当前主界面,而是逐页倒退。

其次是共享状态容易分裂。当前 Index 统一持有 wrongRecords、断点值和安全区值,并根据索引切换内容。如果主页面各自成为独立路由,需要重新决定这些页面的共同容器、导航栏和状态所有权。

最后是重复点击。用户已经位于题库页时再次点击题库 Tab,索引赋相同值不会新增历史;若使用 pushUrl,则可能重复压入同一个页面。

句匠当前做法属于“单主页容器 + 状态切换”:

Column() {
  Stack() {
    this.PageContent()
  }
  .layoutWeight(1)

  Row() {
    this.BottomNavItem(0)
    this.BottomNavItem(1)
    this.BottomNavItem(2)
    this.BottomNavItem(3)
    this.BottomNavItem(4)
  }
}

这一结构也集中处理了底部导航栏和系统安全区。一级页面不用各自重复绘制底栏,选中态也不会分散到五个路由页面里。

需要说明的真实边界是:PageContent() 使用条件分支创建当前组件。文章不据此声称五个页面状态都会永久保留;组件切换后的内部短期状态是否保留,应按 ArkUI 实际生命周期与组件状态设计验证,不能只从 Tab 外观推断。

五、二级页面用 pushUrl 建立可预测返回路径

搜索、分类、统计、设置等页面属于从主页进一步进入的任务页。它们使用 pushUrl,并在页面顶部提供返回入口。

公共 TopBar 的返回按钮实现很直接:

@Component
export struct TopBar {
  title: string = ''
  showBack: boolean = true
  rightIcon?: Resource = undefined
  onRightClick?: () => void = undefined

  build() {
    Row() {
      if (this.showBack) {
        Stack() {
          Image($r('app.media.ic_common_back'))
        }
        .width(46)
        .height(46)
        .onClick(() => {
          router.back()
        })
      } else {
        Blank().width(46)
      }

      Text(this.title)
        .layoutWeight(1)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
  }
}

TopBar 不硬编码“返回到 Index”,而是执行 router.back()。这样从哪里进入,就回到哪里。例如设置页从“我的”进入,返回后仍回到原来的 Index 页面;学习统计若从设置页进入,返回先回设置页,而不是跨层跳回主页。

搜索页没有复用 TopBar,但自定义头部同样调用:

.onClick(() => {
  router.back()
})

从导航语义看,两者一致。工程上若要继续统一视觉和安全区处理,可以让更多二级页复用 TopBar;但某些页面有搜索框等特殊头部,保留自定义组件也合理。统一的是返回行为和层级,不必强迫所有页面拥有完全相同的布局。

六、返回前切换主 Tab:先改状态,再 back

设置页有一个很有代表性的场景:用户从“我的”进入设置,然后点击某个数据入口,希望回到主页的收藏夹或考试 Tab。源码没有在设置页里重新打开一个 Index,而是先修改共享状态,再回退当前路由:

private openFavoriteTab(tabIndex: number): void {
  this.pendingClear = false
  this.favoriteTabIndex = tabIndex
  this.currentTabIndex = 3
  router.back()
}

private openMainTab(tabIndex: number): void {
  this.pendingClear = false
  this.currentTabIndex = tabIndex
  router.back()
}

执行顺序很重要:

  1. 清理设置页的临时确认状态;
  2. 写入目标子 Tab 或主 Tab;
  3. 调用 router.back() 弹出设置页;
  4. 原来的 Index 重新可见,并读取更新后的 AppStorage

如果反过来先 back() 再修改状态,页面生命周期切换可能让后续写入的时机变得难以观察。当前源码用同步赋值后回退,语义清楚。

这种模式适合“二级页完成操作后回到既有主页,并指定一个主区域”。它与 router.replaceUrl({ url: 'pages/Index' }) 不同:后者会重新创建或替换路由记录,可能丢失原有入口层级;前者复用现有 Index,只改变其状态。

前提是设置页确实从包含 Index 的路径进入。当前源码就是这种正常入口。若未来支持设置页被深链直接打开,就不能假定 back() 后一定存在 Index,需要额外定义兜底目标;当前项目没有该深链能力。

七、题库卡片集中决定详情页,而不是让首页判断

首页和题库列表都会复用 BankCard。不同题库可能有专属详情页,其他题库走通用详情页。源码把映射放在卡片组件内部:

private detailPageUrl(): string {
  switch (this.bank.id) {
    case 'b_sichuan':
      return 'pages/SichuanBankPage'
    case 'b_yue':
      return 'pages/YueBankPage'
    case 'b_northeast':
      return 'pages/NortheastBankPage'
    case 'b_shanghai':
      return 'pages/ShanghaiBankPage'
    case 'b_minnan':
      return 'pages/MinnanBankPage'
    case 'b_hakka':
      return 'pages/HakkaBankPage'
    default:
      return 'pages/BankDetailPage'
  }
}

点击卡片时,横向布局和紧凑布局都走同一规则:

.onClick(() => {
  router.pushUrl({
    url: this.detailPageUrl(),
    params: {
      bankId: this.bank.id
    }
  })
})

这避免了首页、题库列表各自维护一份题库 ID 到页面 URL 的映射。BankCard 是实际发起详情导航的组件,因此它拥有“这个题库应该打开哪个详情页”的本地规则。

不过,随着专属页面继续增加,switch 会变长。可落地的收口方向是把映射提取为只读配置,并在没有命中时仍返回 BankDetailPage。无论是否抽取,都要保留默认分支,否则新增题库但忘记配置专属页面时,点击入口会失效。

参数 bankId 始终随路由发送,即使专属详情页看起来已经能从页面名推断题库。这样接收页仍以显式数据为依据,避免把业务 ID隐藏在 URL 命名规则里。

八、SearchPage 对可选参数做温和降级

首页可以有两种方式进入搜索页:

router.pushUrl({ url: 'pages/SearchPage' })

或者:

router.pushUrl({
  url: 'pages/SearchPage',
  params: {
    categoryType: cat.type,
    categoryName: cat.name
  }
})

因此搜索页的参数接口把两个字段都声明为可选:

interface SearchParams {
  categoryType?: string
  categoryName?: string
}

接收时先判断参数对象和关键字段:

aboutToAppear(): void {
  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 时,回退使用 categoryType 作为展示和关键词。一个页面因此支持“空参数入口”和“分类入口”,且不会因为缺少可选展示名而崩溃。

这里的参数校验是轻量的存在性判断,不是运行时 Schema 校验。as SearchParams 只告诉 ArkTS 编译器期望类型,并不会自动阻止外部传入数字或未知字符串。如果项目未来有深链、跨 Ability 或外部 Want 参数,接收边界还需要显式检查 typeof、白名单和长度;当前内部固定入口下,源码只实现了可选值兜底。

九、PracticePage 区分必填参数、默认值和可解析数据

练习页参数比搜索页复杂:

interface PracticeParams {
  bankId: string
  chapterId?: string
  mode: string
  records?: string
}

bankIdmode 在类型上是必填,chapterIdrecords 可选。页面接收后按模式决定数据来源:

aboutToAppear(): void {
  const params = router.getParams() as PracticeParams | undefined
  if (params) {
    this.bankId = params.bankId
    this.mode = params.mode || 'chapter'
    this.chapterId = params.chapterId || ''

    if (this.mode === 'wrongAnalysis') {
      this.loadWrongAnalysis(params)
    } else if (this.mode === 'wrong') {
      // 从错题记录收集题目
    } else if (params.chapterId) {
      this.questions = getQuestionsByChapter(
        params.bankId,
        params.chapterId
      )
    } else {
      this.questions = getQuestions(params.bankId)
    }
  }
}

源码提供了两个实际兜底:

  • mode 为空时回退为 'chapter'
  • chapterId 缺失时回退为空字符串,并走普通题库加载。

错题分析模式还要解析序列化记录:

private loadWrongAnalysis(params: PracticeParams): void {
  let examRecords: AnswerRecord[] = []
  if (params.records) {
    try {
      examRecords =
        JSON.parse(params.records) as AnswerRecord[]
    } catch (_) {
      examRecords = []
    }
  }
  this.records = examRecords.filter(record => !record.correct)
}

JSON 解析失败后使用空数组,避免错误文本直接造成页面异常。这是参数边界里非常实用的一层保护。

同时,当前代码只判断 params 是否存在,没有显式拒绝空 bankId 或未知 mode。内部入口都传入项目定义的值,所以正常流程可用;若要把它提升为统一参数校验,应增加银行 ID 是否存在、模式是否属于允许集合、records 是否真的是记录数组等检查,并在无效时显示错误状态或安全返回。

十、push、replace、back 的选择应围绕任务生命周期

句匠源码中三种操作各有真实使用场景。

pushUrl:进入可以返回的二级任务

首页进入搜索、分类、设置、统计和功能页,题库卡片进入详情,详情页进入练习,通常都希望用户能返回上一层,因此使用 pushUrl

replaceUrl:当前页面不应留在返回栈

Splash 进入 Index:

router.replaceUrl({ url: 'pages/Index' })

考试倒计时结束后,练习页进入结果页:

router.replaceUrl({
  url: 'pages/ExamResultPage',
  params: {
    bankId: this.bankId,
    score,
    total: this.questions.length,
    correct: correctCount,
    durationSec: this.timerSec,
    records: JSON.stringify(this.records)
  }
})

启动页不应该通过返回键重现,已提交的考试页也不应简单返回到仍可继续作答的旧状态。替换路由表达了“当前任务阶段已经结束”。

back:结束当前二级任务

通用顶部栏、搜索页、自定义答题页返回按钮都使用 router.back()。设置页在回退前可以先修改主 Tab 状态。

导航职责与数据边界

API / 状态适用问题句匠实际例子
currentTabIndex同一主页层级切换首页、题库、错题、收藏、我的
pushUrl新增可返回页面搜索、分类、详情、设置
replaceUrl当前阶段结束,不应返回Splash→Index、答题→结果
back结束当前二级页TopBar、搜索头部、设置返回
params给新页面传最小上下文categoryType、bankId、mode

选择 API 时不要只追求调用形式统一,而要统一“返回后用户应该看到什么”。

十一、如何在现有源码上收口导航常量

当前项目直接在各页面写 'pages/SearchPage''pages/CategoryPage' 等字符串,主 Tab 也直接写数字。文章可以基于现状提出最小收口,但不能把下面的建议说成项目已实现。

第一步可以定义路由和 Tab 常量:

export class AppRoutes {
  static readonly SEARCH: string = 'pages/SearchPage'
  static readonly CATEGORY: string = 'pages/CategoryPage'
  static readonly PRACTICE: string = 'pages/PracticePage'
  static readonly SETTINGS: string = 'pages/SettingsPage'
}

export enum MainTab {
  HOME = 0,
  BANK = 1,
  EXAM = 2,
  FAVORITE = 3,
  MINE = 4
}

这一步只消除魔法字符串和数字,不引入复杂框架。调用处仍使用官方 router

this.currentTabIndex = MainTab.BANK

router.pushUrl({
  url: AppRoutes.SEARCH,
  params: {
    categoryType: cat.type,
    categoryName: cat.name
  }
})

第二步只在确有重复时封装参数构造:

interface SearchRouteParams {
  categoryType?: string
  categoryName?: string
}

function openCategorySearch(
  categoryType: string,
  categoryName: string
): void {
  if (categoryType.trim().length === 0) {
    return
  }
  router.pushUrl({
    url: AppRoutes.SEARCH,
    params: {
      categoryType,
      categoryName
    } as SearchRouteParams
  })
}

更重的 NavigationService 只有在需要统一日志、防重复点击、鉴权或错误上报时才值得引入。当前项目规模下,常量、类型和接收端校验已经能解决大部分维护风险。

十二、参数校验应放在接收端最后把关

发送端可以保证项目内部调用正确,但接收端仍是页面的信任边界。尤其是同一页面存在多个入口时,不能只依赖某一个调用方。

以练习页为例,可把模式限制为源码真实使用的集合:

type PracticeMode =
  | 'chapter'
  | 'random'
  | 'exam'
  | 'wrong'
  | 'wrongAnalysis'

function isPracticeMode(value: string): boolean {
  return value === 'chapter' ||
    value === 'random' ||
    value === 'exam' ||
    value === 'wrong' ||
    value === 'wrongAnalysis'
}

接收时再处理:

const params =
  router.getParams() as PracticeParams | undefined

if (!params || typeof params.bankId !== 'string' ||
    params.bankId.trim().length === 0) {
  this.questions = []
  return
}

this.bankId = params.bankId
this.mode = isPracticeMode(params.mode)
  ? params.mode
  : 'chapter'

这段是基于当前参数模型给出的增强建议,不是对现有文件的逐字复刻。它补足了 as PracticeParams 不提供运行时校验的问题。

参数校验要与页面状态结合:

  • 必填 ID 无效:显示错误或空状态,并提供返回;
  • 可选展示名缺失:使用业务类型或默认文案;
  • 模式未知:回退到安全模式;
  • JSON 无法解析:使用空数组,不继续信任其结构;
  • 路由目标未注册:在开发阶段通过页面清单和测试发现。

直接 router.back() 虽然简洁,但如果页面可能作为外部入口,参数无效后的返回目标也要定义。当前句匠没有对外深链,因此本文只把它列为未来边界,不宣称已经支持。

十三、导航验证要覆盖路径组合

单独点击每个按钮并不足以证明导航稳定。更有效的是按路径组合验证:

测试路径预期结果
Splash→Index→题库 Tab不返回 Splash,题库不新增路由记录
首页→搜索→返回回到原首页与原主 Tab
首页题型→带参数搜索自动带入分类并执行搜索
首页题型→缺少 categoryName使用 categoryType 作为显示值
首页→设置→收藏数据入口先退出设置,再显示收藏主 Tab 与目标子 Tab
题库卡片→专属详情→返回返回原题库列表或首页位置
未映射题库→通用详情命中 BankDetailPage 默认分支
练习→提交考试→结果使用 replace,不能回到可继续提交的旧答题状态
错题分析 records 为非法 JSON页面不崩溃,记录回退为空数组
连续快速点击同一入口不出现明显重复页面或多次任务

真实设备还要验证系统返回手势、顶部返回按钮、横竖屏和小窗口。Index 的底部导航已经考虑系统底部避让区,TopBar 读取顶部安全区;导航行为正确但按钮被系统区域遮挡,同样属于不可用。

可以在开发日志中记录目标 URL、参数关键字段和导航结果,但不能打印用户隐私或完整学习内容。对于 records 这种序列化答题数据,更适合记录数量和解析结果,而不是把全部内容写入日志。

十四、常见导航故障与排查顺序

现象优先检查根因方向修复方式
主 Tab 返回键逐页回退是否误用 pushUrl一级页面进入路由栈改为共享索引切换
返回后主页 Tab 不对currentTabIndex 写入时机back 前未设置目标状态先写状态,再 back
点击题库进入错误详情detailPageUrl()ID 映射遗漏或写错核对映射并保留默认页
分类搜索没有自动执行router.getParams()categoryType 缺失或名称不一致对齐字段并做可选兜底
练习页空白bankId、mode、题目集合参数缺失或模式未知接收端校验并显示错误态
错题分析闪退JSON.parse(records)非法 JSON 未捕获try/catch 后使用空数组
结果页返回到已提交答题跳转方式使用 push 保留旧答题页任务完成时使用 replace
返回按钮标题错位TopBar 两侧宽度缺少占位导致标题不居中左右保持等宽占位
快速点击产生重复页面点击节流与当前 URL同一入口连续 push增加短时防重复或状态锁

排查时先确认导航类型,再检查目标路径,最后检查参数。很多“页面数据为空”不是业务查询问题,而是入口根本没有传 bankId;很多“返回异常”也不是系统返回失效,而是上一层页面被错误地替换或重复压栈。

十五、源码边界与可复用结论

从句匠源码能够得到的导航方法不是“所有跳转都封装成一个函数”,而是按层级统一语义:

  1. 主 Tab 由 Index 统一持有,使用 currentTabIndex 切换;
  2. 首页复用卡片只触发调用方传入的动作,不自行猜路由;
  3. 二级任务页使用 pushUrl,返回使用 back
  4. 不应回到的阶段使用 replaceUrl
  5. 带参数页面在接收端提供默认值和解析兜底;
  6. 设置页通过“写共享状态再 back”回到指定主区域;
  7. 题库详情映射集中在实际发起导航的 BankCard
  8. 路由常量、Tab 枚举和更严格校验属于可落地优化,并非当前已完成能力。

在 HarmonyOS 5.0 及以上 ArkTS 项目里,导航可靠性取决于三件事:页面层级是否明确、返回栈是否符合任务生命周期、参数是否在接收边界被检查。句匠当前源码已经具备“主页状态切换 + 二级路由栈 + 部分参数兜底”的基础结构。继续演进时,先收口字符串和数字,再补接收端校验;只有出现真实的日志、鉴权或防重需求时,才增加导航服务层。

AI 辅助声明

本文由 AI 辅助生成和整理,内容已根据 com.jiaweikang.one18 项目中的 Index.etsHomePage.etsSearchPage.etsPracticePage.etsSettingsPage.etsBankCard.etsTopBar.ets 逐项复核;建议性代码已明确标注,能力描述仅覆盖源码中可验证的导航与参数处理范围。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐