部分内容由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、测试、元服务和应用上架分发等。

更多推荐