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

一、首页的目标不是"内容多",而是降低下一步决策成本
中国方言题库当前包名为 com.example.one11,版本配置为 1.0.2,模块声明支持 phone、tablet 和 2in1。应用首页面对的不是普通资讯浏览,而是反复学习场景:新用户需要快速理解题库覆盖范围,老用户需要马上看到自己的进度,明确下一次点击。
源码中的首页按以下顺序组织:
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_QUESTIONS 和 TOTAL_REGIONS 才能用于首页 Hero。题库内容调整后,首页总量会跟随变化,避免"介绍写 1500 题,实际包内只有另一数量"的审核风险。
源码还对题干去重,并把已经下线的听音题重新归类为词汇题,而不是直接丢弃。这说明首页分类数字必须在转换与去重之后计算,不能拿原始素材条数当作用户真正能答的题量。
五、Hero 区展示目录规模与个人状态
首页 Hero 由两类信息组成:
- 目录事实:总题量与覆盖地区数。
- 用户事实:已答题数、正确率、考试次数和收藏数。
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 字段,卡片将数值叠加到封面左下角。但当前源码没有证明它来自在线访问统计。更稳妥的产品处理有两种:
- 把字段明确解释为编辑排序权重,不向用户展示成真实热度。
- 若要展示"热度",说明它是本地预设推荐指数,而非实时用户数。
最不应做的是把 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() 多次,推荐卡片也会查找对应进度。当前数据规模不大,但随着题库和历史增长,重复遍历可能影响滑动和状态更新。
可以从三个方向优化:
- 在服务层把进度数组转换为按
bankId索引的 Map。 - 在学习记录变化时计算一次首页统计快照。
- 题库目录计数只在题库初始化或内容变更时同步,不在每个卡片构建周期重复扫描。
优化前要用真实测量判断瓶颈。不要为了六个题库引入复杂缓存框架,也不要把所有状态塞进全局单例。首页真正需要的是稳定的统计快照与低成本查找。
十四、状态、空态和异常态都要能解释
首页正常态很完整,但工程交付还应显式覆盖:
| 状态 | 页面表现 |
|---|---|
| 首次启动 | 统计全部为 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 以上的多设备应用,这套结构有三个可复用经验:
- 主 Tab 与二级路由分层,避免页面栈混乱。
- 页面列数与卡片内部布局分别响应可用空间。
- 所有统计都从真实本地记录推导,不用静态数字伪造活跃度。
首页不是数据仓库,也不是业务规则中心。它应当是一张可靠的学习仪表盘:用户看到的每个数字都能解释,每个入口都能到达,每种窗口都能继续完成核心任务。
AI 辅助声明: 本文由 AI 辅助整理,所有技术结论基于中国方言题库当前可读取的 ArkTS、模块配置、题库目录和组件源码复核;未运行的真机测试、平台数据和公开发布状态均未写成已通过。