部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于 细胞工坊 项目真实源码展开,源码根目录为 D:\huawei\one14-9。本文重点复核 entry/src/main/ets/pages/LearningPage.etsentry/src/main/ets/views/learning/KnowledgeListPage.etsentry/src/main/ets/views/learning/KnowledgeDetailPage.etsentry/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> 映射表。

知识点列表结构

八、列表卡片做了长文本保护

知识点列表使用 ListForEach 渲染:

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(),会根据 kpIdcategory 选择相关实验:

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 查询
文本在小屏溢出 标题或摘要没限制行数 保留 maxLinestextOverflow
误以为支持搜索 项目有通用 SearchBar 组件但本页没使用 不在列表页声明搜索能力,除非实际接入

这些问题都来自源码的真实结构。当前实现已经适合本地静态知识库,但如果内容规模继续扩大,就需要把分类配置、知识点索引、图标映射和详情查询进一步服务化。

十七、小结:知识列表页的稳定来自三段式链路

细胞工坊的知识点列表链路可以概括为三段:学习中心只负责分类入口,列表页只负责分类过滤和列表导航,详情页负责接收参数并展示内容。KnowledgePoint 提供本地静态知识数组,KnowledgeCategory 提供入口统计和图标颜色,KnowledgeListPagecategory 参数过滤,点击后把知识点字段传给详情页。

这套实现没有伪装成远程知识库,也没有把搜索、收藏、学习进度同步写成已完成能力。它的价值在于清晰、可复核、离线可用。对 HarmonyOS 教学应用来说,先把分类入口、过滤兜底、图标映射、长文本保护和详情跳转做稳定,再扩展搜索、收藏和进度记录,会比一开始堆复杂能力更可靠。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐