【细胞工坊|08】HarmonyOS ArkTS 知识点列表实战:实现细胞、遗传与病毒知识分类
部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于 细胞工坊 项目真实源码展开,源码根目录为 D:\huawei\one14-9。本文重点复核 entry/src/main/ets/pages/LearningPage.ets、entry/src/main/ets/views/learning/KnowledgeListPage.ets、entry/src/main/ets/views/learning/KnowledgeDetailPage.ets 与 entry/src/main/ets/model/KnowledgePoint.ets。文章只讨论源码已经实现的分类入口、知识点过滤、颜色图标映射、列表卡片、详情页参数传递和安全区适配,不虚构搜索、收藏、学习进度同步、远程知识库或 AI 推荐。
知识型页面最容易写成“静态内容堆叠”:首页给几个入口,点进去列一堆标题,再点开看正文。短期看能交付,但分类、详情、图标、颜色和路由参数如果没有边界,后面增加细胞、遗传、病毒、DNA、培养等主题时,页面会迅速变得难维护。细胞工坊的实现采用了一个比较稳的链路:学习中心通过分类卡片传入 category,知识点列表根据分类过滤本地数组,列表项按知识点 ID 映射图标,点击后把知识点字段传给详情页。
这篇文章的重点不是讲生命科学知识本身,而是拆解 HarmonyOS ArkTS 页面如何把知识分类做成可复核、可扩展的本地信息架构。当前源码中的知识点来自 getAllKnowledgePoints() 静态数组,分类信息来自 getKnowledgeCategories(),没有网络请求,也没有后台内容管理。这个边界对 AppGallery 审核和离线教学定位都很重要。

一、先看入口:学习中心负责分类网格,不负责列表过滤
LearningPage 是学习中心,它读取知识分类和学习工具:
@Component
export struct LearningPage {
@State categories: KnowledgeCategory[] = getKnowledgeCategories()
@State tools: LearningTool[] = getLearningTools()
@State screenWidth: number = 360
}
分类入口不是硬编码在 UI 里,而是由 KnowledgePoint.ets 提供:
export interface KnowledgeCategory {
name: string
count: number
icon: Resource
color: string
}
点击分类卡片时,学习中心只做一件事:跳转到知识点列表页,并传入分类名。
router.pushUrl({
url: 'views/learning/KnowledgeListPage',
params: { category: cat.name }
})
这个边界比较清楚。学习中心不需要知道该分类下有哪些知识点,也不负责筛选数组。它只负责展示分类入口。真正的过滤逻辑留给 KnowledgeListPage,这样入口页不会被列表规则污染。

二、分类网格做了响应式列数,适合手机和平板
学习中心的分类网格不是固定两列,而是根据屏幕宽度返回不同列模板:
private gridCols(): string {
if (this.screenWidth > 840) return '1fr 1fr 1fr 1fr'
if (this.screenWidth > 600) return '1fr 1fr 1fr'
return '1fr 1fr'
}
根容器通过 onAreaChange 更新宽度:
.onAreaChange((_o, n) => {
this.screenWidth = n.width as number
})
Grid 使用这个模板:
Grid() {
ForEach(this.categories, (cat: KnowledgeCategory) => {
GridItem() {
Column() {
Image(cat.icon)
Text(cat.name)
Text(`${cat.count}个知识点`)
}
}
})
}
.columnsTemplate(this.gridCols())
.columnsGap(10)
.rowsGap(10)
对 HarmonyOS 多设备应用来说,这种做法比写死两列更稳。手机上两列可读,平板或 2in1 窗口变宽后可以扩展到三列或四列,分类卡片不会被拉得过宽。
三、知识点模型只存可展示字段
知识点数据结构很轻:
export interface KnowledgePoint {
id: string
title: string
category: string
summary: string
content: string
}
它没有收藏状态、学习进度、远程 ID、版本号或富媒体 URL。当前源码定位就是一个本地静态知识库。列表页和详情页都围绕这五个字段展开。
分类统计则由 KnowledgeCategory 提供:
export function getKnowledgeCategories(): KnowledgeCategory[] {
return [
{ name: '基础', count: 12, icon: $r('app.media.ic_bio_microscope'), color: '#00D9FF' },
{ name: '培养', count: 10, icon: $r('app.media.ic_bio_petri'), color: '#00FFB2' },
{ name: 'DNA', count: 9, icon: $r('app.media.ic_bio_dna'), color: '#7C4DFF' },
{ name: '病毒', count: 8, icon: $r('app.media.ic_bio_virus'), color: '#FF4D4F' },
{ name: '遗传', count: 8, icon: $r('app.media.ic_bio_genetics'), color: '#7C4DFF' },
{ name: '细胞', count: 10, icon: $r('app.media.ic_bio_mitosis'), color: '#00FFB2' }
]
}
命令行读取 KnowledgePoint.ets 时部分中文可能显示为乱码,但分类结构、字段和数量可以从源码函数复核。文章只基于这些可验证字段描述分类能力。
四、列表页从路由参数读取分类
KnowledgeListPage 定义了路由参数接口:
interface KnowledgeListParams {
category?: string
}
页面初始化时读取参数:
aboutToAppear(): void {
const params = router.getParams() as KnowledgeListParams | undefined
if (params?.category) {
this.category = params.category
}
this.reloadPoints()
}
这段代码有两个工程点:
| 逻辑 | 作用 | 风险控制 |
|---|---|---|
params?.category |
支持从学习中心传入分类 | 参数不存在时保留默认分类 |
reloadPoints() |
统一刷新列表数据 | 避免 UI 树里写过滤逻辑 |
默认分类是 基础:
@State category: string = '基础'
因此即使页面不是从分类网格进入,而是从首页快捷入口进入,也能展示基础知识点列表,不会出现空白页。
五、过滤逻辑带基础兜底,避免未知分类导致空列表
知识点过滤函数很短,但有明确兜底:
private reloadPoints(): void {
const filtered = this.allPoints.filter((item: KnowledgePoint) => item.category === this.category)
this.points = filtered.length > 0 ? filtered : this.allPoints.filter((item: KnowledgePoint) => item.category === '基础')
}
如果传入的分类存在,就展示该分类;如果传入分类无匹配结果,就退回基础分类。这样做可以防止路由参数拼错、旧版本入口残留或外部快捷入口传入未知分类时页面空白。
这里有一个边界:页面标题仍显示 this.category + ' · 知识点',如果传入未知分类,列表显示基础内容,但标题可能仍是未知分类。后续如果要更严谨,可以在兜底时同步修正 category:
if (filtered.length > 0) {
this.points = filtered
} else {
this.category = '基础'
this.points = this.allPoints.filter((item: KnowledgePoint) => item.category === '基础')
}
这只是可改进方向,当前源码的真实实现是“列表数据兜底到基础,标题不主动重置”。
六、分类颜色用函数集中映射
列表页根据当前分类返回主色:
private getCategoryColor(): string {
const cat = this.category
if (cat === '基础') return '#00D9FF'
if (cat === '培养') return '#00FFB2'
if (cat === 'DNA') return '#7C4DFF'
if (cat === '病毒') return '#FF4D4F'
if (cat === '遗传') return '#7C4DFF'
if (cat === '细胞') return '#00FFB2'
return '#00D9FF'
}
这个函数让列表项图标、数字标牌和分类视觉保持一致。比在每个组件里手写颜色更容易维护。
但这里也有一个可维护性问题:颜色在 getKnowledgeCategories() 和 getCategoryColor() 中各写了一份。后续如果分类色调整,可能出现入口页和列表页颜色不一致。更稳的方式是把分类颜色统一放在一个配置表里,然后学习中心和列表页都读取同一份数据。
七、知识点图标按 ID 映射,不只按分类粗略映射
getPointIcon() 是当前页面较长的一段逻辑。它不是简单按分类给同一个图标,而是按知识点 ID 映射更具体的资源:
private getPointIcon(kp: KnowledgePoint): Resource {
const id = kp.id
if (id === 'basic_1') return $r('app.media.ic_bio_microscope')
if (id === 'basic_2') return $r('app.media.ic_bio_cell')
if (id === 'basic_3') return $r('app.media.ic_bio_protocol')
if (id === 'basic_4') return $r('app.media.ic_bio_pipette')
if (id === 'basic_5') return $r('app.media.ic_bio_shield')
if (id === 'culture_1' || id === 'culture_6' || id === 'culture_7') {
return $r('app.media.ic_bio_petri')
}
if (id === 'dna_3' || id === 'dna_4' || id === 'dna_5') {
return $r('app.media.ic_bio_dna')
}
return $r('app.media.ic_bio_cell')
}
这能让知识点列表更像知识库,而不是同一分类下所有条目都长一样。比如 DNA 提取、PCR、突变、离心分层可以显示不同图标,用户扫列表时更容易区分主题。
代价是函数会越来越长。当前数据规模大约几十条,手写映射还能接受;如果知识点继续增长,建议把图标名放进 KnowledgePoint 数据结构,或者建立 Record<string, Resource> 映射表。

八、列表卡片做了长文本保护
知识点列表使用 List 和 ForEach 渲染:
List({ space: 8 }) {
ForEach(this.points, (kp: KnowledgePoint, index: number) => {
ListItem() {
Row() {
// icon + text + arrow
}
}
})
}
.width('100%')
.layoutWeight(1)
标题和摘要都限制了行数:
Text(kp.title)
.fontSize(AppFonts.BODY_SIZE)
.fontWeight(AppFonts.WEIGHT_MEDIUM)
.fontColor(AppColors.TEXT_PRIMARY)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(kp.summary)
.fontSize(AppFonts.CAPTION_SIZE)
.fontColor(AppColors.TEXT_SECONDARY)
.margin({ top: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
这对知识内容页很重要。知识点摘要天然会比功能按钮文案长,如果不做截断,小屏设备上列表高度会失控,右侧箭头也可能被挤出可视区域。
九、数字标牌提供当前分类内的阅读顺序
列表项左侧使用 Stack 组合背景圆、图标和数字:
Stack({ alignContent: Alignment.BottomEnd }) {
Column() {}
.width(42)
.height(42)
.borderRadius(21)
.backgroundColor(this.getCategoryColor())
.opacity(0.12)
Image(this.getPointIcon(kp))
.width(22)
.height(22)
.fillColor(this.getCategoryColor())
.alignSelf(ItemAlign.Center)
Text(`${index + 1}`)
.fontSize(8)
.fontWeight(AppFonts.WEIGHT_BOLD)
.fontColor(AppColors.TEXT_WHITE)
.backgroundColor(this.getCategoryColor())
.width(15)
.height(15)
.borderRadius(7.5)
.textAlign(TextAlign.Center)
}
这个结构把“主题”和“序号”放在同一视觉区域。序号是当前分类内的顺序,不是全局知识 ID。用户从基础、DNA、病毒切换时,序号会重新从 1 开始,这和 ForEach(this.points, ..., index) 的实现一致。
十、点击列表项把详情所需字段一次性传过去
列表项点击后跳转详情页:
router.pushUrl({
url: 'views/learning/KnowledgeDetailPage',
params: {
id: kp.id,
title: kp.title,
category: kp.category,
summary: kp.summary,
content: kp.content
}
})
当前实现直接把详情页需要的字段作为路由参数传递。这样详情页不需要再从全量知识数组里查一次,页面打开逻辑简单。
代价是路由参数变长。当前内容是本地短文本,还能接受;如果未来知识正文变成长文章、富文本或图片组合,更稳的方式是只传 id,详情页根据 id 查询本地模型或仓储服务。当前源码尚未抽服务层,文章不把它写成已实现能力。
十一、详情页做参数兜底和文本渲染
KnowledgeDetailPage 读取列表页传入的参数:
aboutToAppear(): void {
const params = router.getParams() as KnowledgeDetailParams | undefined
if (params?.id) this.kpId = params.id
if (params?.title) this.title = this.normalizeDisplayText(params.title, '知识点')
if (params?.category) this.category = this.normalizeDisplayText(params.category, '基础')
if (params?.summary) this.summary = this.normalizeDisplayText(params.summary, this.summary)
if (params?.content) this.content = this.normalizeDisplayText(params.content, this.content)
}
默认状态也给了兜底:
@State title: string = '知识点'
@State category: string = '基础'
@State summary: string = '通过分类知识点,快速理解对应生命科学概念。'
@State content: string = '结合实验现象、可视化反馈和生活实例进行理解,可以让知识点更容易记住。'
这意味着即使路由参数缺失,详情页也不会空白。它会显示默认知识点文案。对教学类应用来说,这种兜底比直接崩溃更符合用户预期。
十二、详情页还根据知识点映射相关实验
详情页源码里有 getRelatedExperiment(),会根据 kpId 和 category 选择相关实验:
private getRelatedExperiment(): RelatedExp {
const id = this.kpId
const cat = this.category
let resId = 'microscope_observation'
let resName = '显微镜观察洋葱表皮'
if (cat === '培养') {
if (id === 'culture_2' || id === 'culture_4' || id === 'culture_7' || id === 'culture_10') {
resId = 'sterile_operation'
resName = '无菌操作挑战'
} else {
resId = 'bacteria_culture'
resName = '培养细菌菌落'
}
}
return { id: resId, name: resName }
}
这说明知识点详情不只是静态文本,它已经准备了“知识点 -> 相关实验”的映射逻辑。需要注意的是,文章本文聚焦列表页,相关实验入口是否已经完整出现在详情 UI 中,需要按详情页后续源码继续复核,不能只凭这个函数声称全链路已经打通。
十三、安全区和页面滚动处理方式一致
知识点列表页读取顶部和底部安全区:
@StorageProp('statusBarHeight') statusBarHeight: number = 36
@StorageProp('bottomBarHeight') bottomBarHeight: number = 0
根容器统一避让:
.width('100%')
.height('100%')
.backgroundColor(AppColors.PAGE_BG)
.padding({ top: this.statusBarHeight, bottom: this.bottomBarHeight })
列表区域使用 layoutWeight(1),这样顶部导航固定,列表内容占剩余空间。知识点数量较多时,列表滚动不会把顶部导航挤走,也不会让底部内容直接贴住系统栏。
十四、当前没有实现搜索、收藏和远程更新
从 KnowledgeListPage 源码看,当前知识点列表只做分类过滤和跳转详情。没有搜索框,没有收藏状态,没有请求接口,也没有分页加载。这个边界必须写清楚。
| 能力 | 当前源码是否支持 | 证据 |
|---|---|---|
| 分类入口 | 支持 | LearningPage 分类 Grid |
| 按分类过滤 | 支持 | reloadPoints() |
| 基础兜底 | 支持 | 无匹配时回到基础 |
| 图标映射 | 支持 | getPointIcon() |
| 详情跳转 | 支持 | router.pushUrl() |
| 搜索 | 未实现 | 本页无 SearchBar |
| 收藏 | 未实现 | KnowledgePoint 无收藏字段 |
| 远程更新 | 未实现 | 数据来自静态数组 |
| 学习进度同步 | 未实现 | 本页无 DataStore 写入 |
这张表能帮助读者判断源码真实能力。技术文章不是功能宣传页,不能把后续想做的东西写成已经完成。
十五、验证清单:从分类入口到详情页
复核知识点列表时,可以按下面顺序:
| 步骤 | 操作 | 预期 |
|---|---|---|
| 学习中心 | 打开学习页 | 分类网格显示基础、培养、DNA、病毒、遗传、细胞 |
| 响应式列数 | 改变窗口宽度 | 600、840 宽度阈值后列数变化 |
| 分类跳转 | 点击 DNA 分类 | 进入列表页并带 category: 'DNA' |
| 列表过滤 | 查看列表标题和内容 | 显示 DNA 知识点 |
| 未知分类 | 传入不存在分类 | 列表数据兜底基础分类 |
| 列表长文本 | 查看摘要 | 标题一行、摘要两行省略 |
| 详情跳转 | 点击任意知识点 | 打开详情页并带 id/title/category/summary/content |
| 返回 | 点击顶部返回 | 调用 router.back() |
如果未来加入搜索或收藏,需要额外验证搜索关键字空态、收藏状态持久化、返回后状态恢复等场景。当前源码还不涉及这些能力。
十六、常见问题与处理方式
| 问题 | 常见原因 | 处理建议 |
|---|---|---|
| 分类入口数量和列表实际数量不一致 | getKnowledgeCategories() 的 count 手写后未同步 |
从 getAllKnowledgePoints() 统计生成,减少手工维护 |
| 传入未知分类标题异常 | 列表数据兜底但 category 未重置 |
兜底时同步设置 this.category = '基础' |
| 图标映射函数过长 | 所有 ID 判断集中在页面方法 | 抽成映射表或把 icon 放进模型 |
| 详情参数过长 | 直接把 content 放入路由参数 | 只传 id,详情页按 id 查询 |
| 文本在小屏溢出 | 标题或摘要没限制行数 | 保留 maxLines 和 textOverflow |
| 误以为支持搜索 | 项目有通用 SearchBar 组件但本页没使用 | 不在列表页声明搜索能力,除非实际接入 |
这些问题都来自源码的真实结构。当前实现已经适合本地静态知识库,但如果内容规模继续扩大,就需要把分类配置、知识点索引、图标映射和详情查询进一步服务化。
十七、小结:知识列表页的稳定来自三段式链路
细胞工坊的知识点列表链路可以概括为三段:学习中心只负责分类入口,列表页只负责分类过滤和列表导航,详情页负责接收参数并展示内容。KnowledgePoint 提供本地静态知识数组,KnowledgeCategory 提供入口统计和图标颜色,KnowledgeListPage 用 category 参数过滤,点击后把知识点字段传给详情页。
这套实现没有伪装成远程知识库,也没有把搜索、收藏、学习进度同步写成已完成能力。它的价值在于清晰、可复核、离线可用。对 HarmonyOS 教学应用来说,先把分类入口、过滤兜底、图标映射、长文本保护和详情跳转做稳定,再扩展搜索、收藏和进度记录,会比一开始堆复杂能力更可靠。
更多推荐


所有评论(0)