【中国方言题库|01】HarmonyOS ArkTS 方言题库首页实战:组织地区入口、推荐内容和学习进度

摘要:题库应用的首页不是把所有功能堆在一屏,而是要回答三个问题:用户上次学到哪里、现在最值得做什么、怎样用最短路径进入目标题库。本文基于"中国方言题库"真实 ArkTS 源码,拆解 HomePageIndexMockBanksBankCard 的协作方式,展示如何用 ArkUI 组织六个地区题库、七类题型、本地学习统计、快速入口以及 phone、tablet、2in1 的断点布局。所有题量和学习进度都来自当前题库与本地记录,不虚构在线热度、排行榜或云端推荐。

一、首页的目标不是"内容多",而是降低下一步决策成本

中国方言题库当前包名为 com.example.one11,版本配置为 1.0.2,模块声明支持 phonetablet2in1。应用首页面对的不是普通资讯浏览,而是反复学习场景:新用户需要快速理解题库覆盖范围,老用户需要马上看到自己的进度,明确下一次点击。

源码中的首页按以下顺序组织:

text 复制代码
品牌与搜索
-> 今日练习概览
-> 继续练习与快速操作
-> 题型分类
-> 推荐题库

这个顺序有明确的信息优先级。标题负责建立产品身份;Hero 区给出题量和用户状态;快速操作把"继续学习"放到最短路径;分类适合按题型探索;推荐题库则按地区提供完整入口。用户不必先理解应用所有页面,就能开始一次答题。

本文的唯一标记是:首页统计必须由本地学习记录实时推导。题量、已答、正确率、考试次数和收藏数都应能回到实际数据源,不能用装饰性数字填满界面。

二、主导航先解决跨设备结构

Index.ets 定义五个主入口:

ts 复制代码
private tabs: TabItem[] = [
  { title: '首页', iconNormal: $r('app.media.ic_tab_home'), iconSelected: $r('app.media.ic_tab_home_selected') },
  { title: '题库', iconNormal: $r('app.media.ic_tab_bank'), iconSelected: $r('app.media.ic_tab_bank_selected') },
  { title: '报考', iconNormal: $r('app.media.ic_tab_exam'), iconSelected: $r('app.media.ic_tab_exam_selected') },
  { title: '收藏', iconNormal: $r('app.media.ic_tab_favorite'), iconSelected: $r('app.media.ic_tab_favorite_selected') },
  { title: '我的', iconNormal: $r('app.media.ic_tab_mine'), iconSelected: $r('app.media.ic_tab_mine_selected') }
]

当前选中项通过 @StorageLink('currentTabIndex') 共享。首页中的"更多""继续练习""错题复习"等入口可以直接切换主 Tab,而搜索、分类等二级功能使用 router.pushUrl() 进入独立页面。这形成两个清晰层级:

  • 主功能切换:修改 currentTabIndex,不增加页面栈。
  • 二级任务:使用 Router 跳转,支持系统返回。

在窄窗口中,Index 使用底部导航;在更宽断点中改为左侧导航栏。底部导航高度并非写死在系统手势区上方,而是读取 navigationIndicatorHeightPx,经过 px2vp() 转换后与最小安全距离取最大值:

ts 复制代码
private bottomSafePadding(): number {
  return Math.max(
    Sizes.BOTTOM_NAV_MIN_PADDING,
    this.getUIContext().px2vp(this.navigationIndicatorHeightPx)
  )
}

这种做法比固定增加一段 padding 更可靠,因为不同设备、导航模式和窗口状态的避让高度可能不同。

三、首页状态只连接"需要显示的数据"

HomePage 通过 @StorageLink 订阅四类学习记录:

ts 复制代码
@StorageLink('bankProgress') progressList: BankProgress[] = []
@StorageLink('examHistory') examHistory: ExamHistory[] = []
@StorageLink('favoriteRecords') favRecords: FavoriteRecord[] = []
@StorageLink('wrongRecords') wrongRecords: WrongRecord[] = []

首页没有直接操作 Preferences,也没有自己解析数据库文件,而是把已有状态交给 StatService.summarize()

ts 复制代码
private myStats() {
  return StatService.summarize(
    this.progressList,
    this.examHistory,
    this.favRecords,
    this.wrongRecords
  )
}

这条边界很重要。页面负责展示,统计规则应在服务层;持久化读取应在数据管理层。否则每个页面都各自计算正确率,很容易出现首页、题库详情和"我的"页面数字不一致。

对于 ArkUI 状态,还要注意数组更新方式。如果服务只是修改原数组中对象的字段,却没有触发可观察状态更新,页面可能不会刷新。稳妥做法是在数据服务完成写入后,以明确的新数组或统一状态发布方式更新 AppStorage,让 Hero、题库卡片和徽标一起变化。

四、题库目录来自真实题目集合

MockBanks.ets 中定义六个地区:

ts 复制代码
export const REGIONS: Region[] = [
  { id: 'sichuan', name: '四川话', shortName: '川', cover: $r('app.media.img_bank_cover_sichuan') },
  { id: 'yue', name: '粤语', shortName: '粤', cover: $r('app.media.img_bank_cover_yue') },
  { id: 'northeast', name: '东北话', shortName: '东', cover: $r('app.media.img_bank_cover_northeast') },
  { id: 'shanghai', name: '上海话', shortName: '沪', cover: $r('app.media.img_bank_cover_shanghai') },
  { id: 'minnan', name: '闽南语', shortName: '闽', cover: $r('app.media.img_bank_cover_minnan') },
  { id: 'hakka', name: '客家话', shortName: '客', cover: $r('app.media.img_bank_cover_hakka') }
]

题型包含词汇、看义猜词、方言差异、情景对话、方言文化、俗语歇后语和地区特色。源码不是把目录卡片上的题量永久写成一个常数,而是在 syncCatalogCounts() 中从题目集合回填每个题库、章节和分类的数量。

ts 复制代码
function syncCatalogCounts(): void {
  const categoryCounts: Map<string, number> = new Map<string, number>()
  for (const bank of BANKS) {
    const questions = getQuestions(bank.id)
    bank.totalCount = questions.length
    for (const chapter of bank.chapters) {
      chapter.total = questions.filter(
        (question: Question) => question.chapterId === chapter.id
      ).length
    }
    for (const question of questions) {
      categoryCounts.set(
        question.type,
        (categoryCounts.get(question.type) || 0) + 1
      )
    }
  }
}

由此导出的 TOTAL_QUESTIONSTOTAL_REGIONS 才能用于首页 Hero。题库内容调整后,首页总量会跟随变化,避免"介绍写 1500 题,实际包内只有另一数量"的审核风险。

源码还对题干去重,并把已经下线的听音题重新归类为词汇题,而不是直接丢弃。这说明首页分类数字必须在转换与去重之后计算,不能拿原始素材条数当作用户真正能答的题量。

五、Hero 区展示目录规模与个人状态

首页 Hero 由两类信息组成:

  1. 目录事实:总题量与覆盖地区数。
  2. 用户事实:已答题数、正确率、考试次数和收藏数。
ts 复制代码
Text(`${TOTAL_QUESTIONS.toLocaleString()} 题`)
Text(`覆盖 ${TOTAL_REGIONS} 个地区方言`)

this.HeroStat(`${this.myStats().totalAnswered}`, '已答')
this.HeroStat(`${this.myStats().accuracyPercent}%`, '正确率')
this.HeroStat(`${this.myStats().examCount}`, '考试')
this.HeroStat(`${this.myStats().favoriteCount}`, '收藏')

这里没有把"热门度"当成学习成果,也没有写一个静态正确率。新用户没有记录时应自然显示 0,而不是用样例数据制造"应用很活跃"的错觉。若正确率分母为 0,统计服务应返回 0,不能出现 NaN%

Hero 的地图图片使用固定视觉尺寸并配合 objectFit(ImageFit.Cover),文字区域则由 Blank() 分隔。适配时要特别测试系统字体放大,因为 32vp 的题量和右侧 108vp 图片在窄屏上可能竞争空间。若长数字或本地化文案导致拥挤,可以让图片缩小、限制文字行数,或在极窄宽度切换为上下结构。

六、快速操作要对应真实可达路径

首页的主行动"继续练习"通过切换到题库 Tab 实现:

ts 复制代码
.onClick(() => { this.currentTabIndex = 1 })

第二行提供"真实题量计时测试"和"错题复习"。错题复习会同时设置收藏页内部 Tab:

ts 复制代码
this.favoriteTabIndex = 2
this.currentTabIndex = 3

这种联动很适合主导航内的跨模块入口,但必须保证目标页面也使用同一个 @StorageLink('favoriteTabIndex'),否则首页写入了状态,收藏页却仍停留在默认分栏。回归测试至少覆盖:

  • 无错题时点击"错题复习"出现明确空态。
  • 有错题时进入收藏页的错题分栏。
  • 从错题页返回首页后,主导航状态正确。
  • 连续快速点击不会叠加路由或丢失选中状态。

"继续练习"当前只是进入题库列表,并没有读取最近一次题库直接恢复章节,因此文案使用"从热门题库开始"是合理的。如果未来要真正实现续学,应保存最近题库和章节 ID,并在目标不存在时降级到题库列表,不能只把按钮改名为"继续上次"。

七、分类入口使用可复核的参数契约

首页显示七个题型和一个"查看全部",每个分类通过 Router 传递类型与名称:

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

这里的契约是 categoryType 用于筛选,categoryName 用于展示。目标页应对参数缺失、未知类型和旧版本书签做兜底。如果直接断言参数一定存在,外部恢复页面或路由重建时可能白屏。

分类项宽度为 23%,四列换行,并设置单行省略。它能在常见手机宽度上形成规整网格,但"俗语歇后语"这类长名称可能被省略。省略虽然避免布局破坏,却可能降低可辨认性。可选改进包括:

  • 允许两行并给容器稳定高度。
  • 使用更短但准确的显示名。
  • 在平板宽度下增加列数或提升项目宽度。
  • 为图标提供明确的无障碍描述。

不要通过把字体缩到不可读来维持四列。AppGallery 的正文和关键文字对比、字号与可触达性比"每行必须四个"更重要。

八、推荐题库不是网络推荐算法

首页"推荐题库"实际遍历本地 BANKS

ts 复制代码
ForEach(BANKS, (bank: Bank) => {
  BankCard({ bank: bank })
}, (bank: Bank) => bank.id)

因此这里的"推荐"含义是产品预设的地区入口,而不是基于账号画像、云端行为或全网热度的个性化推荐。商店介绍和隐私政策不应把它描述成 AI 推荐。

题库模型中有 hot 字段,卡片将数值叠加到封面左下角。但当前源码没有证明它来自在线访问统计。更稳妥的产品处理有两种:

  1. 把字段明确解释为编辑排序权重,不向用户展示成真实热度。
  2. 若要展示"热度",说明它是本地预设推荐指数,而非实时用户数。

最不应做的是把 98、95 等静态值写成"98 万人在学"。任何用户量、下载量、排名或趋势都需要真实平台数据。

九、BankCard 将题库事实和用户进度合并

BankCard 同时显示题库封面、题量、正确率、已答数量和进度条。它不是直接采用题库模型里的默认准确率,而是优先读取用户真实进度:

ts 复制代码
private realAccuracy(): number {
  const p = UserDataManager.getProgress(this.progressList, this.bank.id)
  if (p && p.finished > 0) return p.correct / p.finished
  return this.bank.accuracy
}

private progressRatio(): number {
  if (this.bank.totalCount === 0) return 0
  return Math.min(this.realFinished() / this.bank.totalCount, 1)
}

这里有两个细节值得保留:

  • 分母为 0 时返回 0,避免异常值。
  • 使用 Math.min(..., 1),避免题库调整后旧进度超过新题量导致进度条溢出。

还可以进一步处理下限:若损坏数据出现负数,建议把比例限制在 [0, 1]。正确率也应验证 correct <= finished,数据修复应在服务层完成,而不是让每张卡片各自猜测。

十、卡片按自身宽度切换,而不是只看设备名

题库卡片通过 onAreaChange 记录实际宽度:

ts 复制代码
@State cardWidth: number = 360

private useCompactLayout(): boolean {
  return this.cardWidth > 0 && this.cardWidth < 280
}

宽度小于 280vp 时使用纵向紧凑卡片,否则使用横向卡片。这个策略比"平板永远横向、手机永远纵向"更适合多窗口:同一台 2in1 设备在窄窗口和宽窗口中可以采用不同卡片。

首页的推荐区也根据断点和页面实际宽度决定布局:

ts 复制代码
private useGridLayout(): boolean {
  return this.currentBp === 'lg' && this.pageWidth >= 700
}

宽布局中每张卡片占 32%,形成三列;其他情况使用单列。这是"页面决定列数、卡片决定内部形态"的两级自适应:

  • 页面容器决定一行放几张。
  • 卡片根据自己得到的宽度决定横向或紧凑布局。

这种职责分配可复用性更高。BankCard 不必知道自己位于首页、搜索结果还是收藏页,只需要响应可用空间。

十一、为何首页末尾仍保留 92vp 空白

HomePage 的内容末尾有:

ts 复制代码
Blank().height(92)

这是为了让最后一张题库卡片在底部导航上方仍能滚动到可见位置。但固定 92vp 与 Index 中动态计算的导航安全高度存在潜在重复:如果手势区更高,空白可能仍不足;如果侧边导航模式没有底部栏,空白又显得过多。

更一致的方案是把底部内容 inset 作为共享状态或布局参数传给页面:

ts 复制代码
Scroll() {
  // content
}
.contentEndOffset(this.bottomContentInset)

具体 API 需按项目 SDK 可用性选取,也可以让 Index 的内容容器统一留出空间。关键是底部导航与页面末尾不要分别维护两个互不相干的常数。

十二、搜索入口要保持功能边界清楚

Header 右侧搜索按钮进入 pages/SearchPage。搜索属于二级任务,因此使用路由而不是主 Tab。按钮当前用 Stack 包裹图片,并提供 46vp 的触控区域,满足常用触控尺寸。

发布前还应验证:

  • 搜索图标在浅色和深色背景下有足够对比。
  • 无输入时有推荐或历史,而不是空白页。
  • 不存在匹配项时显示明确空态。
  • 输入法弹出后,搜索结果和返回按钮仍可触达。
  • 从分类入口进入时,标题和筛选条件正确。

如果搜索仅针对本地题库,就不应因为模块声明了 INTERNET 而偷偷改成在线联想;网络权限的实际用途必须单独从源码核实,并在隐私材料中准确说明。

十三、性能重点不是"少写组件",而是控制重复计算

首页每次重建都会调用 myStats() 多次,推荐卡片也会查找对应进度。当前数据规模不大,但随着题库和历史增长,重复遍历可能影响滑动和状态更新。

可以从三个方向优化:

  1. 在服务层把进度数组转换为按 bankId 索引的 Map。
  2. 在学习记录变化时计算一次首页统计快照。
  3. 题库目录计数只在题库初始化或内容变更时同步,不在每个卡片构建周期重复扫描。

优化前要用真实测量判断瓶颈。不要为了六个题库引入复杂缓存框架,也不要把所有状态塞进全局单例。首页真正需要的是稳定的统计快照与低成本查找。

十四、状态、空态和异常态都要能解释

首页正常态很完整,但工程交付还应显式覆盖:

状态 页面表现
首次启动 统计全部为 0,推荐题库仍可进入
无收藏/错题 对应计数为 0,点击后显示空态
题库为空 总题量为 0,练习入口禁用或说明原因
本地数据损坏 使用安全默认值并记录诊断信息
路由参数错误 回退到题库列表,不白屏
图片资源失败 保持卡片尺寸,显示替代背景

当前目录内容来自包内数据,不需要把题库加载包装成虚假的网络 Loading。若初始化确实有异步 Preferences 恢复,可以在首屏使用稳定骨架或延迟展示个人统计,但地区入口不必等待用户数据后才能出现。

十五、无障碍和国际化不能最后补

方言学习应用包含大量具有文化语境的文本。首页至少要为搜索图标、题库封面和可点击卡片提供可理解的无障碍信息。只显示"川""粤"等短字时,读屏描述应是"四川话题库""粤语题库",而不是单个字符。

虽然当前产品以中文为主,也应避免通过字符串拼接破坏后续本地化。例如:

ts 复制代码
Text(`${TOTAL_QUESTIONS.toLocaleString()} 题`)

在单一中文版本中可用,但更完整的工程可以使用字符串资源和占位符。应用名、模块说明、隐私文案与 AppGallery 素材也应统一引用当前品牌"中国方言题库"。

十六、首页测试应围绕数据变化和窗口变化

建议把首页回归拆成以下用例:

数据用例

  • 新用户统计为 0。
  • 完成一道题后,已答和正确率变化。
  • 收藏一道题后,收藏计数变化。
  • 产生错题后,主导航徽标与错题入口同步。
  • 删除历史后,考试次数更新。
  • 题库总量变更后,总题量和卡片题量一致。

交互用例

  • 搜索按钮进入搜索页并可返回。
  • 每个分类传递正确 categoryType
  • "更多"切换到题库 Tab。
  • "计时测试"切换到报考 Tab。
  • "错题复习"打开收藏页错题分栏。
  • 六张题库卡片进入各自详情页。

适配用例

  • 手机底部导航不进入手势区。
  • 平板/2in1 使用侧边导航。
  • 700vp 附近窗口缩放时,推荐区切换稳定。
  • 卡片宽度跨过 280vp 时内部布局不闪烁。
  • 系统字体放大后标题、统计和长题型名仍可读。

十七、从首页源码能得出的边界结论

基于当前真实实现,可以准确描述:

  • 首页覆盖六个地区方言题库。
  • 首页提供七类题型入口和全部分类入口。
  • 总题量来自去重、归类后的本地题目集合。
  • 已答、正确率、考试和收藏来自本地用户记录汇总。
  • 推荐题库是本地产品排序,不是云端个性化推荐。
  • 支持 phone、tablet、2in1,并在不同宽度使用底部或侧边导航。
  • 题库卡片按实际宽度切换横向与紧凑结构。

不能从首页源码直接声称:

  • 有多少真实在线用户。
  • 题库热度是实时平台统计。
  • 支持云同步、账号漫游或 AI 推荐。
  • 所有声明设备都已经完成真机测试。
  • 当前工作区版本已经在 AppGallery 公开。

技术文章和上架文案都应守住这条事实边界。

十八、总结

中国方言题库首页的核心设计不是视觉装饰,而是把目录事实、学习状态和下一步行动压缩到一屏:Index 负责多设备主导航,HomePage 负责信息顺序,MockBanks 负责真实目录与题量,StatService 负责学习汇总,BankCard 负责题库事实与用户进度的响应式呈现。

对于 HarmonyOS 5.0 以上的多设备应用,这套结构有三个可复用经验:

  1. 主 Tab 与二级路由分层,避免页面栈混乱。
  2. 页面列数与卡片内部布局分别响应可用空间。
  3. 所有统计都从真实本地记录推导,不用静态数字伪造活跃度。

首页不是数据仓库,也不是业务规则中心。它应当是一张可靠的学习仪表盘:用户看到的每个数字都能解释,每个入口都能到达,每种窗口都能继续完成核心任务。


AI 辅助声明: 本文由 AI 辅助整理,所有技术结论基于中国方言题库当前可读取的 ArkTS、模块配置、题库目录和组件源码复核;未运行的真机测试、平台数据和公开发布状态均未写成已通过。

相关推荐
OH_TPC3 小时前
HarmonyOS APP开发---"壁纸控"壁纸App,需要用到这个库
harmonyos
独守一片天3 小时前
鸿蒙穿戴设备与健康服务闭环
华为·harmonyos
Magic-ZYJ4 小时前
隐私优先 HarmonyOS 应用怎么设计:无账号、无后台、无统计 SDK
华为·harmonyos·鸿蒙·独立开发者·心晴手记
独守一片天4 小时前
鸿蒙车机手机协同服务生态
华为·智能手机·harmonyos
2501_919749035 小时前
华为鸿蒙测手速APP—小羊手速
华为·harmonyos·鸿蒙
Magic-ZYJ6 小时前
HarmonyOS 调用系统文件保存器导出 JSON 与 CSV
华为·json·harmonyos·鸿蒙·独立开发者
潘高8 小时前
一款叫《班妥了》的鸿蒙原生 + 微信小程序双端排班应用
harmonyos
梦想不只是梦与想9 小时前
鸿蒙应用集成“集成态hsp“的兼容
harmonyos·api兼容性·集成态hsp
大雷神11 小时前
HarmonyOS 私密相册实战:一张照片移入保险箱后,怎样让普通相册真的看不见
harmonyos