鸿蒙原生应用实战:用 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 这一个文件,从架构、主题、布局、组件、数据、交互六个维度完整拆解。


项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

二、整体架构:首页在工程中的位置

应用入口是 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)
}

几个值得注意的细节:

  1. Scroll 隐藏滚动条(scrollBar(BarState.Off)),符合现代 App 的极简趋势,内容滚动时不会出现灰色滚动条破坏画面。
  2. 底部 padding 预留 safeBottom + 20,防止最后一条试卷被底部 Tab 栏遮挡,是沉浸式布局的标准做法。
  3. 区块间统一 space: 16,与系列其他 App 的"区块留白"节奏保持一致,页面看起来疏密有致。

下面这张图是首页首屏的真实运行效果:从上到下依次是双行 Header、4 列课程 Grid、热门资料标题、前两条试卷卡和底部 Tab 栏:

校园考试资料库首页首屏 · 考试红双行Header(考试资料库/真题题库助你高分💯)+4列课程Grid(📐高数/🌍英语/💻数据结构/🧪物理)+热门资料(📝高等数学期末真题 高等数学2025年892浏览/📄大学英语四级模拟卷 756/📋数据结构期中试卷 2024年634/📊线性代数复习题库 521+⭐收藏)


三、全局主题系统:考试红 #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,页面其他区域保持不动:

高数课程卡交互 · 点击 4 列课程 Grid 第一格(📐 高数)后屏幕底部弹出"高数"Toast,热门资料列表与底部 Tab 保持显示


六、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标题课程年份浏览
📝高等数学期末真题高等数学2025892
📄大学英语四级模拟卷大学英语2025756
📋数据结构期中试卷数据结构2024634
📊线性代数复习题库线性代数2025521

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 文案的差异,证明两个事件确实分离:

高数期末真题点击交互 · 点击热门资料第一条"高等数学期末真题"(高等数学 2025年 892浏览)后屏幕底部弹出"高等数学期末真题"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课本共享搜索框 + Banner2 列商品 Grid(现价 + 划线价)买什么书
37考试资料库双行品牌头 + slogan4 列课程 Grid + 热门试卷(浏览数 + 收藏)做哪套真题

37 首页把系列的成熟手法组合出了自己的味道:双行 Header 沿袭 35/36,课程入口从 35 的横滑改为 4 列 Grid,列表项沿用"emoji 头像 + 双行文字 + 右侧操作"的系列范式,但数据维度换成"年份 + 浏览数",并首次引入 ⭐ 快捷收藏的嵌套点击。每一个差异都指向同一个结论:首页形态 = 业务模式,考试题库的核心是"快速决策做哪套题"。


十一、工程化细节:ArkTS 严格模式

整个 HomeTab 是系列工程规范的完整样本:

  1. 零 any:所有数据结构用 interface 显式声明,编译器在 IDE 阶段就能发现字段拼写错误;
  2. keyGenerator 必填:两处 ForEach 都提供第三参数,满足严格模式要求;
  3. @Builder 拆分:5 个 UI 区块各自独立,build() 一眼可读,复用与维护都清晰;
  4. 主题 token 统一:颜色、圆角、间距全部取自 C / D,将来"考试红想换成理工蓝",只需改 C.primary 一个值。

十二、可演进方向

首页骨架完整,距离产品级还有几条自然的演进路径:把 papers 接入真实接口并做下拉刷新;课程 Grid 与资料 Tab 打通(点高数直接跳高数章节列表,而非 toast);⭐ 收藏改为 ⭐/☆ 二态切换并同步到"我的"页的收藏资料;试卷卡跳转详情页展示 PDF 预览与下载;按年份/课程筛选热门资料;搜索页结果与首页热门数据打通。这些改造大多不需要重写 UI,只需要在"状态 → 渲染"的链路里补数据和触发点。


十三、写在最后

回到开篇的问题:如何让学生 3 秒选课、5 秒选卷?

App 37 的答案是用 113 行 ArkTS 代码,构建一个"双行品牌头 + 4 列课程 Grid + 热门资料列表"的三段式首页:Header 用"助你高分 💯"立住价值承诺,课程 Grid 用 emoji 完成零成本分类导航,试卷列表用浏览数排序建立信任、用 ⭐ 嵌套收藏提供快捷操作。没有炫技,没有多余装饰,每一行代码都在解决"考前找真题"这一个具体问题。

这正是一套可以直接复用的"学习资料类首页模板"——换成题库、讲义、网课资源,只需替换数据与 slogan。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐