鸿蒙原生应用实战:用 ArkUI 搭考试资料库首页 —— 课程 Grid + 热门资料双列
鸿蒙原生应用实战:用 ArkUI 搭考试资料库首页 —— 课程 Grid + 热门资料双列
本文以 HarmonyOS 6.0(API 23)+ ArkTS 声明式 UI 为技术底座,逐行拆解「校园考试资料库」应用首页(
HomeTab)的设计理念、布局结构与交互细节。所有截图均来自 HarmonyOS 模拟器实机画面。核心源码为37-exam-bank/entry/src/main/ets/pages/HomeTab.ets(全文 113 行)。
一、项目缘起:为什么是"考试资料库"
大学生活里最刚性的内容需求,恐怕就是"考前找真题"。期末周来临,每个宿舍群都在流传同一句话:"有没有去年的高数卷子?"——试卷真题散落在学长学姐的网盘里、班级群文件里、打印店的 U 盘里,找到一份靠谱的近年真题往往要花掉大半个晚上。
「校园考试资料库」正是为这个场景而生:把真题、模拟卷、题库、复习提纲按课程和章节组织起来,让"找题"从人肉翻聊天记录变成"打开 App 就能刷"。它由底部四个 Tab 组成:首页(📊)、资料(📑)、搜索(🔍)、我的(👤)。其中首页承载两条核心任务——让用户 3 秒选课程、5 秒选试卷:上半屏用 4 列课程 Grid 提供分类入口,下半屏用按浏览数排序的热门资料列表直接给出"大家都在看什么"的答案。
下文围绕 HomeTab.ets 这一个文件,从架构、主题、布局、组件、数据、交互六个维度完整拆解。
二、整体架构:首页在工程中的位置
应用入口是 Index.ets,一个 @Entry 装饰的 @Component,用 Tabs 容器承载四个 TabContent,分别挂载 HomeTab、Func1Tab、Func2Tab、ProfileTab:
Tabs({ barPosition: BarPosition.End, index: this.current }) {
TabContent() { HomeTab() }.tabBar(this.bar('首页', '📊', 0))
TabContent() { Func1Tab() }.tabBar(this.bar('资料', '📑', 1))
TabContent() { Func2Tab() }.tabBar(this.bar('搜索', '🔍', 2))
TabContent() { ProfileTab() }.tabBar(this.bar('我的', '👤', 3))
}
.barBackgroundColor('#FFFFFF')
.barHeight(60 + this.safeBottom)
Tab 栏高度是 60 + this.safeBottom——safeBottom 来自 EntryAbility.ets 里的安全区探测:应用启动时设置全屏布局,通过 getWindowAvoidArea 拿到底部导航条高度,px2vp 换算后存入 AppStorage。HomeTab 用两个 @StorageProp 订阅,保证折叠屏、旋转等场景下 UI 自动重新对齐。这是整个系列通用的"看不见的工程",也是页面能"像成熟产品"的基础。
HomeTab 自身的结构非常清晰:最外层 Column 的上半部分是固定不滚动的 Header(),下半部分是占满剩余高度的 Scroll,内部用 Column({ space: 16 }) 依次堆叠三个区块:HotCourses(课程入口)→ SectionTitle('热门资料') → PaperList(试卷列表):
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 16 }) {
this.HotCourses()
this.SectionTitle('热门资料')
this.PaperList()
}
.width('100%')
.padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
几个值得注意的细节:
Scroll隐藏滚动条(scrollBar(BarState.Off)),符合现代 App 的极简趋势,内容滚动时不会出现灰色滚动条破坏画面。- 底部 padding 预留
safeBottom + 20,防止最后一条试卷被底部 Tab 栏遮挡,是沉浸式布局的标准做法。 - 区块间统一
space: 16,与系列其他 App 的"区块留白"节奏保持一致,页面看起来疏密有致。
下面这张图是首页首屏的真实运行效果:从上到下依次是双行 Header、4 列课程 Grid、热门资料标题、前两条试卷卡和底部 Tab 栏:

三、全局主题系统:考试红 #EB5757
所有颜色与尺寸统一收在 common/Theme.ets 的两个 class 里。与系列其他 App 不同,37 号把主色定为 #EB5757 的考试红:
static readonly primary: string = '#EB5757';
static readonly primarySoft: string = '#FDEAEA';
primary 是"考试红",primarySoft 是它的浅色版(10% 左右的红,接近淡粉)。这个配色的选择是有讲究的:
- 红色 = 批改的红笔。试卷上老师的红色批注、对勾和叉,是每个学生最熟悉不过的视觉符号,用它做考试类产品的主色几乎不需要教育成本;
- 红色 = 重点标注。划重点、标考点用的都是红色记号笔,天然传达"这里重要、要考"的信息;
- 红色 = 冲刺与紧张。考试周的紧迫感、倒计时的压迫感,都落在红色的情绪区间里,与"助你高分"的激励 slogan 形成张力。
对比系列内其他 App:App 31 饮品用珊瑚红(#FF6B6B)传递"新鲜好喝"的愉悦,App 37 用更偏玫红的 #EB5757 传递"考试重点"的紧张——同样是红色系,饱和度与场景完全不同。这正是"主题色 = 业务模式"的直接体现。
D 类提供三档圆角(20/14/10)与统一内边距 16,页面里所有卡片的圆角都从 D.rMd(14)取数,不写魔法数字。
四、Header:双行品牌头
@Builder
Header() {
Column() {
Text('考试资料库').fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text)
.width('100%').padding({ top: this.safeTop + 10, left: D.pad, right: D.pad, bottom: 4 })
Text('真题题库,助你高分 💯').fontSize(12).fontColor(C.textDim).width('100%')
.padding({ left: D.pad, right: D.pad, bottom: 12 })
}.width('100%').backgroundColor(C.card)
}
Header 只有两行文字:22 号加粗的"考试资料库"主标题 + 12 号灰的"真题题库,助你高分 💯"副标题,白底。主标题直接点明产品身份,副标题则完成价值主张的传达:"真题题库"交代内容形态,"助你高分"给出结果承诺,💯 emoji 把抽象承诺具象成"100 分"——三个词把"这是一个能帮你拿高分的真题 App"说透了。
顶部 padding 用 safeTop + 10,让标题永远刚好落在状态栏下方 10vp 处;两行之间的间距由主标题 bottom: 4 与副标题 bottom: 12 共同构成,底部留白 12vp 给下方滚动区一个清晰的呼吸空间。
与系列对比:App 35 笔记社区的 slogan 是"分享学习笔记",App 36 课本共享走的是"搜索框 + 促销"的电商头,App 37 用"双行品牌头 + 激励文案"——同样是 Header,形态完全跟随业务模式:内容社区卖氛围、电商卖转化、考试 App 卖承诺。
五、HotCourses:4 列课程 Grid
课程入口是 App 37 首页区别于 35/36 的特色区块。数据是极简的 CourseChip 接口(2 字段:icon + name),4 门核心课程:
interface CourseChip {
icon: string;
name: string;
}
@Builder
HotCourses() {
Grid() {
ForEach(this.hotCourses, (item: CourseChip, idx: number) => {
GridItem() {
Column({ space: 6 }) {
Text(item.icon).fontSize(28)
Text(item.name).fontSize(12).fontColor(C.text)
}
.width('100%').padding({ top: 14, bottom: 14 }).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.onClick(() => { promptAction.showToast({ message: item.name }); })
}
}, (item: CourseChip, idx: number) => item.name)
}.columnsTemplate('1fr 1fr 1fr 1fr').columnsGap(10).width('100%')
}
布局:columnsTemplate('1fr 1fr 1fr 1fr') 声明四等分列,配合 columnsGap(10),四门课(📐 高数 / 🌍 英语 / 💻 数据结构 / 🧪 物理)一行排开。每格是一个 Column({ space: 6 }):28 号 emoji 在上、12 号课程名在下,上下 padding 各 14vp,白底 + 14 圆角 + 1px 浅描边——与下方试卷卡使用同一套"白卡"语言,视觉上浑然一体。
emoji 即图标:📐 数学的尺规、🌍 语言的地球、💻 编程的电脑、🧪 实验的烧瓶,每个 emoji 都精准对应学科语义,零资源成本完成课程识别,这是整个系列"emoji 视觉体系"的延续。
点击反馈:onClick 弹 toast 展示课程名,是"可点但未实装"的标准占位写法——真实产品里点击应进入该课程的资料页。与 App 35 的课程横滑卡片相比,37 用 4 列 Grid 一次性暴露全部课程,信息密度更高、无需滑动,适合"只有 4 门课"的轻量场景;若课程数量增长,columnsTemplate 改两行即可扩展。
下面这张图演示点击第一格 📐 高数后的效果——底部弹出"高数"Toast,页面其他区域保持不动:

六、SectionTitle:区块标题的通用组件
@Builder
SectionTitle(title: string) {
Row() {
Text(title).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('更多 >').fontSize(12).fontColor(C.textDim)
}.width('100%')
}
Blank() 把标题和"更多 >"推向两端:左侧 16 号加粗的区块名,右侧 12 号灰的入口提示。这个组件在系列中反复出现(App 35/36 的资料区标题同款),参数化的 title: string 让它一次编写处处复用。"更多 >"虽无 onClick,但"看到 > 就想点"的潜意识引导本身就是一种交互设计——这是用户研究中的经典手法。
七、PaperList:热门资料列表(本页核心)
如果说课程 Grid 是首页的"入口",那 PaperList 就是首页的"内容"——它直接承担"选试卷"的核心转化。数据是 Paper 接口的 4 条记录:
interface Paper {
id: number;
emoji: string;
title: string;
course: string;
year: string;
views: number;
}
| emoji | 标题 | 课程 | 年份 | 浏览 |
|---|---|---|---|---|
| 📝 | 高等数学期末真题 | 高等数学 | 2025 | 892 |
| 📄 | 大学英语四级模拟卷 | 大学英语 | 2025 | 756 |
| 📋 | 数据结构期中试卷 | 数据结构 | 2024 | 634 |
| 📊 | 线性代数复习题库 | 线性代数 | 2025 | 521 |
6 个字段正好覆盖"决策所需的全部信息":标题、归属课程、年份(时效性)、浏览数(热度),加上 emoji 做视觉封面、id 做列表 key。
@Builder
PaperList() {
Column({ space: 12 }) {
ForEach(this.papers, (p: Paper) => {
Row({ space: 12 }) {
Row() { Text(p.emoji).fontSize(26) }
.width(48).height(48).backgroundColor(C.primarySoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(p.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(p.course + ' · ' + p.year + '年 · ' + p.views + ' 浏览').fontSize(12).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('⭐').fontSize(18)
.onClick(() => { promptAction.showToast({ message: '收藏: ' + p.title }); })
}
.width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke })
.onClick(() => { promptAction.showToast({ message: p.title }); })
}, (p: Paper) => p.id.toString())
}.width('100%')
}
每张试卷卡可以拆成"左中右"三部分:
左侧 48×48 浅红头像:一个 C.primarySoft(#FDEAEA)底的圆角方块,居中放 26 号 emoji。浅红底是考试红 10% 的淡化,让每张卡都带上一抹"考试色",与主题呼应又不抢内容——这是系列统一的"列表头像"范式,换成其他 App 只是换底色和 emoji。
中间文字区:Column({ space: 4 }) 装两行。第一行是 14 号 Medium 的试卷标题,maxLines(1) + textOverflow: Ellipsis 保证长标题单行省略、卡片高度整齐;第二行是 12 号灰的元信息,用 · 中点分隔"课程 · 年份 · 浏览数"。这里的数据设计很考究:
- 浏览数递减排序(892 → 756 → 634 → 521)——"热门 = 浏览多"是内容平台的通用信任逻辑,高浏览数天然具备从众说服力,用户会优先点开"大家都在看"的卷子;
- 年份标注(2025/2025/2024/2025)——真题的价值在于时效,近年真题优先展示,2024 的数据结构期中卷被排第三,暗示"年份越新越靠前"的排序心智。
右侧 ⭐ 收藏按钮:18 号 emoji 独立成列,是最精妙的交互设计——嵌套点击。整卡 onClick 负责"看详情",⭐ 的 onClick 负责"收藏",两个点击区域互不干扰:点 ⭐ 只收藏不跳转,点卡片只看详情不收藏。这在 ArkUI 里无需任何事件拦截代码,子组件自带点击能力天然优先响应。真实产品中 ⭐ 应切换收藏态(⭐/☆ 二态切换),demo 里用 toast 占位。
下面两张图分别演示两种点击路径:点卡片弹"高等数学期末真题",点 ⭐ 弹"收藏: 高等数学期末真题"——注意 toast 文案的差异,证明两个事件确实分离:


八、数据模型与状态管理
整个 HomeTab 只有 2 个 @StorageProp(safeTop / safeBottom)和 2 个私有数据数组,没有 @State——因为首页当前没有任何需要交互改变的状态,UI 纯粹是数据的静态映射。这正是声明式范式的优雅之处:将来某天要加"按年份筛选",只需引入一个 @State activeYear,UI 会自动跟随重渲染。
两个 interface 值得一提:
Paper(6 字段)——试卷实体的最小可用集。id 是列表渲染的 key,emoji 是视觉封面,title/course/year/views 覆盖决策信息。未来加downloads(下载数)、pages(页数)、fav(是否已收藏)都不需要改结构,字段天然可扩展。CourseChip(2 字段)——课程入口的最小模型。icon + name 足以渲染整个 Grid。
ForEach 第三参数 keyGenerator(p.id.toString() / item.name)是 ArkTS 严格模式的强制要求,它告诉框架如何唯一标识每一项,保证后续局部刷新只重绘变化的单元格。
九、交互与体验设计分析
三级信息架构:整个首页是 L1 品牌(Header)→ L2 分类(课程 Grid)→ L3 内容(试卷列表)的渐进式披露,用户从"这是什么 App"到"看什么课"再到"做哪套题",每层只给当前决策所需的信息,不铺开全部资料。
数据背书而非运营位:首页没有 Banner、没有弹窗,热度完全靠浏览数说话。892/756/634/521 的递减序列既是 demo 数据的"真实感"来源,也是内容平台的信任机制——把"推荐什么"的决定权交给数据,用户反而更容易下单。
跨页数据呼应:首页的 4 门课程(高数/英语/数据结构/物理)与资料 Tab 的课程胶囊一一对应;首页的"高等数学期末真题"与搜索 Tab 的热门搜索词"高数期末"形成闭环——首页引导"高数 → 找卷",搜索页直接命中"高数期末",demo 数据的一致性设计让整个 App 看起来像一个真实产品。
色彩一致性:考试红贯穿全页——课程卡、列表头像的浅红底、Tab 选中色,用户在任意位置都能感知"这是考试 App",强化品牌记忆。
十、与系列对比:35 笔记 / 36 课本 / 37 考试
| App | 首页主题 | Header 形态 | 内容区 | 决策 |
|---|---|---|---|---|
| 35 | 笔记社区 | 双行品牌头 | 课程横滑 + 热门笔记(点赞) | 看什么笔记 |
| 36 | 课本共享 | 搜索框 + Banner | 2 列商品 Grid(现价 + 划线价) | 买什么书 |
| 37 | 考试资料库 | 双行品牌头 + slogan | 4 列课程 Grid + 热门试卷(浏览数 + 收藏) | 做哪套真题 |
37 首页把系列的成熟手法组合出了自己的味道:双行 Header 沿袭 35/36,课程入口从 35 的横滑改为 4 列 Grid,列表项沿用"emoji 头像 + 双行文字 + 右侧操作"的系列范式,但数据维度换成"年份 + 浏览数",并首次引入 ⭐ 快捷收藏的嵌套点击。每一个差异都指向同一个结论:首页形态 = 业务模式,考试题库的核心是"快速决策做哪套题"。
十一、工程化细节:ArkTS 严格模式
整个 HomeTab 是系列工程规范的完整样本:
- 零
any:所有数据结构用interface显式声明,编译器在 IDE 阶段就能发现字段拼写错误; - keyGenerator 必填:两处
ForEach都提供第三参数,满足严格模式要求; @Builder拆分:5 个 UI 区块各自独立,build()一眼可读,复用与维护都清晰;- 主题 token 统一:颜色、圆角、间距全部取自
C/D,将来"考试红想换成理工蓝",只需改C.primary一个值。
十二、可演进方向
首页骨架完整,距离产品级还有几条自然的演进路径:把 papers 接入真实接口并做下拉刷新;课程 Grid 与资料 Tab 打通(点高数直接跳高数章节列表,而非 toast);⭐ 收藏改为 ⭐/☆ 二态切换并同步到"我的"页的收藏资料;试卷卡跳转详情页展示 PDF 预览与下载;按年份/课程筛选热门资料;搜索页结果与首页热门数据打通。这些改造大多不需要重写 UI,只需要在"状态 → 渲染"的链路里补数据和触发点。
十三、写在最后
回到开篇的问题:如何让学生 3 秒选课、5 秒选卷?
App 37 的答案是用 113 行 ArkTS 代码,构建一个"双行品牌头 + 4 列课程 Grid + 热门资料列表"的三段式首页:Header 用"助你高分 💯"立住价值承诺,课程 Grid 用 emoji 完成零成本分类导航,试卷列表用浏览数排序建立信任、用 ⭐ 嵌套收藏提供快捷操作。没有炫技,没有多余装饰,每一行代码都在解决"考前找真题"这一个具体问题。
这正是一套可以直接复用的"学习资料类首页模板"——换成题库、讲义、网课资源,只需替换数据与 slogan。

更多推荐





所有评论(0)