ArkTS原生 | 知识问答引擎 —— 鸿蒙Next声明式UI实战

ArkTS原生 | 知识问答引擎 ------ 鸿蒙Next声明式UI实战

一、项目背景与概述

在鸿蒙生态快速发展的当下,ArkTS作为鸿蒙原生应用开发的首选语言,凭借其声明式UI框架、强类型系统以及深度的系统能力集成,正逐步被广大开发者接受和采用。知识问答引擎项目正是在这一背景下诞生的------它是一套完全基于ArkTS原生能力构建的轻量级知识问答功能模块,不依赖任何第三方UI库或框架,纯粹利用鸿蒙ArkUI的Grid、List、Search等原生组件,实现了包含分类标签云、关键词搜索高亮、多维度过滤等功能的知识问答页面。

本文将从工程实践的角度,逐层拆解该模块的架构设计、组件实现、状态管理以及ArkTS语法约束下的应对策略,旨在为鸿蒙开发者提供一个可参考、可复用的实战范例。

1.1 功能需求概览

  • 分类标签云:以Grid网格形式展示12个知识分类,每个分类包含图标、名称和条目数,支持点击选中/取消选中进行过滤。
  • 问答条目列表:以卡片式List展示问答条目,包含标题、摘要、标签、浏览数、点赞数、日期等信息。
  • 关键词搜索:输入关键词后实时过滤,并在标题和摘要中对匹配文字进行高亮标记。
  • 组合过滤:搜索关键词与分类过滤可叠加使用,"清除过滤"一键重置所有筛选条件。

1.2 技术选型决策

选择纯ArkTS原生实现而非引入第三方库,主要基于以下考量:

  • 零依赖:避免npm包版本冲突和鸿蒙兼容性问题。
  • 编译优化:ArkTS编译器对原生组件有深度优化,性能更优。
  • 包体积:无外部依赖意味着更小的hap包体积。
  • API一致性:随鸿蒙版本升级同步更新,无需等待第三方库适配。

二、系统架构设计

整个模块采用组件树层级架构,从数据层到视图层共分为三个层次。

复制代码
┌─────────────────────────────────────────┐
│               QApage (页面容器)           │
│  ┌─────────────────────────────────────┐ │
│  │         Search (搜索组件)            │ │
│  ├─────────────────────────────────────┤ │
│  │   Grid (分类标签云, 4列x3行)         │ │
│  │   ├── GridItem × 12               │ │
│  └─────────────────────────────────────┘ │
│  ┌─────────────────────────────────────┐ │
│  │  List (问答条目列表)                 │ │
│  │   ├── ListItem → QACard            │ │
│  │   │   ├── HighlightText (标题)     │ │
│  │   │   └── 摘要 + 标签 + 统计       │ │
│  └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

2.1 数据流方向

ArkTS采用单向数据流 + 装饰器驱动的响应式更新:

  1. 数据源定义CATEGORIESQA_LIST 作为顶层常量数据。
  2. 状态持有QApage 通过 @State 持有 categoriesoriginListfilteredListsearchKeywordselectedCategoryId 等可变状态。
  3. 过滤逻辑 :用户交互(搜索输入/分类点击)触发 applyFilter(),遍历 originList 生成新的 filteredList
  4. UI响应@State 数据变更自动触发 build() 重新渲染,子组件通过 @Prop 接收数据。

2.2 组件职责划分

组件 职责 数据输入
QApage 页面容器、状态管理、过滤逻辑、布局编排 全局常量数据
QACard 单条问答卡片渲染,触发摘要高亮 QAItemkeyword
HighlightText 文本关键词高亮渲染 textkeyword

这种"父组件管状态、子组件管渲染"的分工模式,符合ArkTS推荐的最佳实践。

三、数据模型设计

3.1 分类数据模型

typescript 复制代码
interface Category {
  id: number;
  name: string;
  count: number;     // 该分类下问题数
  icon: string;      // 图标 emoji
  isSelected: boolean;
}

Category 接口包含了展示标签云所需的全部信息。isSelected 字段用于控制网格项的高亮状态样式切换。模拟数据覆盖了HarmonyOS开发的12个核心领域:

分类 条目数 覆盖主题
HarmonyOS 24 分布式、Ability调度
ArkTS 18 装饰器、状态管理
UI组件 32 Grid、List、弹窗
网络编程 15 HTTP、WebSocket
数据管理 21 分布式数据对象
多媒体 10 相机、XComponent
传感器 8 设备能力
安全 12 HUKS加密
性能优化 14 SmartPerf、启动优化
测试 9 单元测试
动画 16 转场、自定义弹窗
AI能力 7 语音识别

3.2 问答条目数据模型

typescript 复制代码
interface QAItem {
  id: number;
  title: string;
  summary: string;
  categoryId: number;
  tags: string[];
  viewCount: number;
  likeCount: number;
  date: string;
}

每个问答条目通过 categoryId 与分类关联,构成了典型的一对多关系。14条模拟问答数据覆盖了各分类的真实技术问题,标题采用中文问句形式,增强了内容的可读性和真实感。

3.3 数据深拷贝策略

typescript 复制代码
@State categories: Category[] = JSON.parse(JSON.stringify(CATEGORIES));
@State originList: QAItem[] = JSON.parse(JSON.stringify(QA_LIST));

使用 JSON.parse(JSON.stringify(...)) 进行深拷贝,确保 @State 持有的数据不与顶层常量共享引用,避免意外的副作用。

四、核心组件实现详解

4.1 HighlightText ------ 关键词高亮引擎

HighlightText 是本次实现中最精妙的小型组件,它承担着在文本中定位关键词并包裹高亮样式的任务。在Web开发中这通常由 dangerouslySetInnerHTML 或正则替换完成,但在ArkTS的 Text 组件中,必须利用 Text() 及其子组件的组合来实现。

4.1.1 设计思路

高亮文本的渲染策略可概括为:用关键词分割原字符串,然后按"普通文本 → 关键词 → 普通文本"的顺序依次渲染 Text 片段。其中关键词部分使用红色字体 + 红色背景,实现视觉上的高亮效果。

4.1.2 关键实现
typescript 复制代码
@Builder
KeywordHighlightText(parts: string[], keyword: string) {
  ForEach(parts, (part: string, index: number) => {
    if (index === 0) {
      Text(part)                       // 第一个片段总是普通文本
    } else {
      Text(keyword)                    // 高亮关键词
        .fontColor('#ff6b6b')
        .backgroundColor('#ffd4d4')
      Text(part)                       // 关键词后的普通文本
    }
  })
}

这里有一个精妙的设计点:string.split(keyword) 的返回值中,第一个元素(index === 0)总是普通文本,之后每次遇到关键词时,索引加1对应关键词后的普通文本片段。因此 ForEach 中,index === 0 时只渲染普通文本,index > 0 时先渲染关键词高亮,再渲染普通文本。

4.1.3 ArkTS语法约束的应对

最初的设计思路是在 build() 中直接写 let parts = this.text.split(this.keyword),但ArkTS编译器严格禁止在 build()@Builder 方法中出现 let 声明。解决方案是将计算逻辑提取到普通方法中:

typescript 复制代码
getParts(): string[] {
  if (this.keyword.length === 0) return [this.text];
  return this.text.split(this.keyword);
}

然后在 build() 中调用 this.KeywordHighlightText(this.getParts(), this.keyword) --- 将计算结果作为参数传递给 @Builder 方法。这种"计算在方法、渲染在build"的模式是ArkTS开发中的核心技巧。

4.2 QACard ------ 问答卡片组件

QACard 负责渲染单条问答条目的卡片式布局,包含标题、摘要、标签和统计信息四个区域。

4.2.1 标题区

标题区直接复用 HighlightText 组件:

typescript 复制代码
HighlightText({ text: this.item.title, keyword: this.keyword })
  .margin({ bottom: 8 });

由于 HighlightText 内已处理了关键词不存在时的兜底逻辑(直接渲染纯文本),父组件无需额外判断。

4.2.2 摘要区

摘要区的实现比标题更为复杂,因为它需要处理更长的文本和可能的关键词匹配。QACard提供了三个辅助方法来为build()提供数据:

  • getSummaryParts():按关键词分割摘要文本,返回字符串数组。
  • getHasKeyword():判断摘要中是否包含关键词,决定是否启用高亮渲染。
  • getPartsCount():计算分割后的片段数(为后续扩展预留)。

build() 中根据 getHasKeyword() 的结果决定渲染方式:

typescript 复制代码
if (this.getHasKeyword()) {
  Text() {
    this.SummaryHighlight(this.getSummaryParts(), this.keyword);
  }
  .maxLines(2)
  .textOverflow({ overflow: TextOverflow.Ellipsis });
} else {
  Text(this.item.summary)
    .maxLines(2)
    .textOverflow({ overflow: TextOverflow.Ellipsis });
}

值得注意的设计细节 :使用了 .maxLines(2) + .textOverflow(TextOverflow.Ellipsis) 来限制摘要最多显示两行,超出部分以省略号截断。这是移动端卡片布局的标准交互模式。

4.2.3 标签与统计区

底部的标签和统计信息占据一行,使用弹性布局 Row() + Blank() 实现左右分布:

typescript 复制代码
Row() {
  // 左侧标签
  ForEach(this.item.tags, (tag: string) => { ... })
  Blank()                     // 弹性填充
  // 右侧统计
  Row() { Text('👁️') ... }   // 浏览数
  Row() { Text('👍') ... }    // 点赞数
  Text(this.item.date)        // 日期
}

每个标签使用 backgroundColor('#e8f0fe') 配合 borderRadius(4) 形成蓝色药丸样式,与主内容的白色背景形成层次对比。统计信息前的 emoji 图标代替了纯文字标注,使界面更加轻量化。

4.2.4 卡片样式

卡片的整体风格采用圆角白色卡片 + 轻微阴影:

typescript 复制代码
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
  radius: 6,
  offsetX: 0,
  offsetY: 2,
  color: 'rgba(0, 0, 0, 0.06)',
})

.shadow() 的属性值经过精心调校:radius: 6 产生柔和弥散效果,offsetY: 2 营造轻微的悬浮感,color 使用低透明度黑色而非纯灰色,在深色模式下表现更自然。

4.3 QApage ------ 页面主组件

QApage 是整个模块的入口和大脑,负责状态管理、事件处理和布局编排。它注册为 @Entry 组件,作为页面路由的目标。

4.3.1 状态定义
typescript 复制代码
@State categories: Category[] = ...;
@State originList: QAItem[] = ...;    // 原始完整数据
@State filteredList: QAItem[] = ...;  // 过滤后数据
@State searchKeyword: string = '';
@State selectedCategoryId: number = -1;

采用双列表模式originList 作为不可变数据源,filteredList 作为UI直接消费的数据。每次过滤操作都重新遍历 originList 生成新的 filteredList,而非在原数据上逐次缩小范围,避免了二次过滤时状态不一致的问题。

4.3.2 搜索组件适配
typescript 复制代码
Search({
  value: this.searchKeyword,
  placeholder: '搜索问题、关键词...',
  controller: this.searchInputController
})

ArkTS原生 Search 组件提供搜索输入框功能。在实际开发中需要注意:

  • placeholder 属性必须作为构造函数参数传入,不支持链式调用 .placeholder()
  • fontSize 等文本样式属性在某些API版本中不支持链式调用,需移除。
  • onCancel 回调在某些API版本中不存在,需改用其他方案(如清空搜索时触发 onChange)。
  • 通过 .onChange() 回调实时监听输入变化并触发过滤。
4.3.3 Grid分类标签云

分类标签云使用 4列 × 3行 的 Grid 布局展示12个分类:

typescript 复制代码
Grid() {
  ForEach(this.categories, (cat: Category) => {
    GridItem() {
      Column() {
        Text(cat.icon)    // emoji图标
        Text(cat.name)    // 分类名称
        Text(`${cat.count}`) // 条目数
      }
      ...
      .onClick(() => { this.selectCategory(cat); })
    }
  })
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.height(120)

columnsTemplate('1fr 1fr 1fr 1fr') 定义四列均分宽度,rowsGapcolumnsGap 设置8vp的间隔。每个 GridItem 通过点击事件调用 selectCategory() 实现选中/取消选中切换。

分类项的样式变化通过状态驱动:

typescript 复制代码
.backgroundColor(cat.isSelected ? '#007aff' : '#f2f2f7')
.fontColor(cat.isSelected ? Color.White : '#333333')

选中时变为蓝色主题(蓝色背景 + 白色文字),未选中时为浅灰色背景。

4.3.4 问答条目列表

采用 List + ForEach + ListItem + QACard 的组合渲染问答列表:

typescript 复制代码
List({ space: 0 }) {
  ForEach(this.filteredList, (item: QAItem) => {
    ListItem() {
      QACard({ item: item, keyword: this.searchKeyword });
    }
  })
}
.layoutWeight(1)

.layoutWeight(1) 使 List 占据页面的剩余空间,确保列表内容不足时底部不会留白。

五、过滤与搜索逻辑

5.1 核心过滤算法

applyFilter() 方法实现了组合过滤的核心逻辑,支持关键词搜索与分类过滤的同时作用:

typescript 复制代码
applyFilter(): void {
  let keyword: string = this.searchKeyword.trim().toLowerCase();
  let catId: number = this.selectedCategoryId;

  let result: QAItem[] = [];
  for (let i: number = 0; i < this.originList.length; i++) {
    let item: QAItem = this.originList[i];
    // 优先按分类过滤
    if (catId !== -1 && item.categoryId !== catId) continue;
    // 若有搜索关键词,在标题/摘要/标签中匹配
    if (keyword.length > 0) {
      let inTitle = item.title.toLowerCase().includes(keyword);
      let inSummary = item.summary.toLowerCase().includes(keyword);
      // 标签匹配检测...
      if (inTitle || inSummary || inTags) result.push(item);
    } else {
      result.push(item);
    }
  }
  this.filteredList = result;
}

算法特点:

  1. 分类优先过滤 :先检查分类筛选条件,不匹配的直接 continue,避免后续无意义的字符串匹配。
  2. 大小写不敏感:搜索关键词和文本内容均统一转为小写再比较,提升搜索体验。
  3. 多字段搜索:标题、摘要、标签三个字段同时匹配,提高召回率。
  4. 实时响应 :通过 Search.onChange 实时触发,每次输入变化都重新过滤。

5.2 分类选择逻辑

selectCategory() 方法实现了单选式分类选择:

typescript 复制代码
selectCategory(cat: Category): void {
  if (this.selectedCategoryId === cat.id) {
    this.selectedCategoryId = -1;     // 已选中的再次点击取消
  } else {
    this.selectedCategoryId = cat.id;  // 切换为新分类
  }
  // 同步更新所有分类的 isSelected 状态
  for (let i = 0; i < this.categories.length; i++) {
    this.categories[i].isSelected = (
      this.categories[i].id === this.selectedCategoryId
    );
  }
  this.applyFilter();
}

交互细节:再次点击已选中的分类会取消选中(回到"全部"状态),通过 selectedCategoryId = -1 实现。

5.3 清除过滤

当页面处于过滤状态时(有搜索关键词或选中了分类),右上角会出现"清除过滤"链接,一键重置所有条件:

typescript 复制代码
clearFilter(): void {
  this.searchKeyword = '';
  this.selectedCategoryId = -1;
  this.searchInputController.caretPosition(0);  // 光标归位
  // 重置所有分类选中状态
  for (let i = 0; i < this.categories.length; i++) {
    this.categories[i].isSelected = false;
  }
  this.applyFilter();
}

六、ArkTS语法约束与工程实践

在开发过程中,ArkTS语法约束带来了一些独特的挑战,这里总结了几条在实践中验证有效的应对策略。

6.1 build()中的变量声明限制

问题 :ArkTS编译器规定 build() 方法内只能包含组件声明、@Builder 调用、if/else 条件、ForEach 循环等特定语法结构,不允许出现 letconst 等变量声明语句。

应对 :将所有计算逻辑提取到独立的成员方法中,在 build() 中仅调用方法获取返回值:

typescript 复制代码
// ❌ 错误:build() 中声明变量
build() {
  let parts = this.text.split(this.keyword); // 编译错误
}

// ✅ 正确:提取到方法中
getParts(): string[] {
  return this.text.split(this.keyword);
}
build() {
  this.KeywordHighlightText(this.getParts(), this.keyword);
}

6.2 组件属性的链式调用限制

问题 :某些系统组件(如 Search)的属性在特定API版本中不支持链式 .xxx() 调用,部分属性必须通过构造函数参数传入。

应对 :查阅鸿蒙API文档确认每个属性的正确调用方式。一般来说,核心配置属性(如 valueplaceholdercontroller)优先放在构造函数中,样式属性使用链式调用:

typescript 复制代码
// ✅ 正确:构造函数参数 + 链式样式
Search({
  value: this.searchKeyword,
  placeholder: '搜索...',
  controller: this.searchInputController
})
.backgroundColor('#f2f2f7')
.borderRadius(20);

6.3 ForEach的使用规范

问题 :在 @Builder 中使用 ForEach 时,回调函数的参数必须明确标注类型。

应对 :始终为 ForEach 的回调参数添加显式类型标注:

typescript 复制代码
ForEach(this.categories, (cat: Category, index: number) => { ... })

6.4 数据不可变性

问题@State 装饰的数组,当修改其中某个元素的属性时,ArkTS 可能无法检测到深层变化。

应对 :在修改分类的 isSelected 状态时,通过索引直接赋值而非重新创建数组,因为ArkTS的 @State 对数组元素的属性变化有深度观测能力:

typescript 复制代码
for (let i = 0; i < this.categories.length; i++) {
  this.categories[i].isSelected = (
    this.categories[i].id === this.selectedCategoryId
  );
}

七、UI样式设计分析

7.1 色彩系统

页面采用简约的双色主题:

用途 色值 使用场景
主色调 #007aff 分类选中态、链接、标签文字
高亮色 #ff6b6b / #ffd4d4 关键词搜索高亮(红字红底)
文字主色 #1a1a2e 标题、主内容
文字辅色 #666666 摘要
文字浅色 #999999 / #cccccc 统计信息、日期
背景 #f5f5f5 页面底色
卡片色 #ffffff 问答卡片背景

7.2 布局与间距

页面采用20vp的左右边距统一对齐(列表中内层卡片使用16vp内边距),各区块间通过12~8vp的间距分隔,形成清晰的视觉层次。

7.3 圆角与阴影

  • 搜索框:borderRadius(20) 全圆角
  • 分类项:borderRadius(12) 中等圆角
  • 问答卡片:borderRadius(12) + 阴影
  • 标签:borderRadius(4) 小圆角

圆角体系从大到小形成了"搜索框 > 分类项/卡片 > 标签"的层级递减,符合视觉权重。

八、性能优化思考

8.1 避免不必要的渲染

当前实现中,每次搜索输入都会触发 applyFilter() 并更新 filteredList,这会触发整个 List 重新渲染。对于14条数据的规模,性能完全在可接受范围内。但当数据量扩展到成百上千条时,可以考虑:

  • 使用 LazyForEach 替代 ForEach,实现虚拟滚动。
  • 引入防抖(debounce)机制,搜索输入结束后再进行过滤。
  • 使用 @Monitor@Watch 精确控制状态更新的触发条件。

8.2 数据源的选择

采用 originList 作为不可变数据源、每次重新遍历过滤的策略,虽然在时间复杂度上是 O(n),但保证了过滤逻辑的正确性和可预测性。对于中小规模数据集(千条以内),这正是推荐的做法。

九、扩展与展望

9.1 可扩展功能

  • 网络数据源:将模拟数据替换为HTTP请求,接入RESTful API或GraphQL。
  • 分页加载 :在 List 底部添加 .onReachEnd() 回调,实现滚动加载更多。
  • 问答详情页:点击卡片跳转到详情页,展示完整问答内容。
  • 收藏功能:添加收藏状态,支持收藏夹管理。
  • 富文本渲染:问答内容支持代码块、表格等Markdown格式渲染。

9.2 组件抽象优化

当前的 HighlightText 组件只支持单关键词高亮,可扩展为支持多关键词高亮版本:

typescript 复制代码
@Prop keywords: string[] = [];

通过遍历关键词数组,对文本进行递归分割和渲染。

十、总结

本文从工程实战的角度,完整呈现了一个基于 ArkTS 原生能力的知识问答页面的开发全过程。从数据模型设计、组件层级架构、关键词高亮算法,到ArkTS语法约束下的应对策略,再到UI样式和性能优化,覆盖了鸿蒙原生应用开发的核心环节。

通过这个项目,我们可以看到:

  1. ArkTS原生组件能力已经足够强大------仅凭 Grid、List、Search、Text 等基础组件,配合装饰器驱动和状态管理,就能构建出交互流畅、视觉优雅的知识问答页面。
  2. 声明式UI的开发范式带来了显著的效率提升------数据驱动视图、状态自动响应,让开发者可以专注于业务逻辑而非DOM操作。
  3. ArkTS语法约束虽然严苛,但并非限制,而是引导------强制将计算逻辑从build()中分离,客观上促进了组件代码的更清晰解耦和更易维护。

在未来的鸿蒙生态中,ArkTS不仅是开发应用的工具,更是构建鸿蒙原生体验的基石。掌握其声明式UI的编程范式和组件的组合艺术,是每一位鸿蒙开发者进阶的必经之路。


项目代码仓库 :该页面的完整源码位于项目 entry/src/main/ets/pages/QApage.ets,可直接在 DevEco Studio 中打开编译运行。

技术栈:ArkTS + ArkUI (HarmonyOS API 11+)

关键词:HarmonyOS、ArkTS、ArkUI、知识问答、搜索高亮、Grid标签云、List列表、声明式UI

相关推荐
坚果派·白晓明3 小时前
鸿蒙 PC 应用集成 libhv 鸿蒙化三方库 —— AtomCode + Skills 驱动的高效集成实践
c语言·c++·ai编程·harmonyos·atomcode
Java知识技术分享3 小时前
opencode安装ui-ux-pro-max和frontend-ui-ux技能
人工智能·ui·个人开发·ai编程·ux
祭曦念4 小时前
【共创季稿事节】HarmonyOS动态任务列表开发实战
华为·harmonyos
祭曦念5 小时前
【共创季稿事节】鸿蒙原生ArkTS动态列表布局实战_State_ForEach完整指南
华为·harmonyos
不羁的木木5 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第二篇:核心功能——查询与监听握持手状态
华为·harmonyos
风华圆舞5 小时前
鸿蒙 + Flutter 下 AI 页面的状态协同设计
人工智能·flutter·harmonyos
互联网散修6 小时前
鸿蒙实战:仿小红书“我”的页面——从分层架构到沉浸式交互
交互·harmonyos
里昆6 小时前
【illustrator】如何在illustrator中画箭头
ui·illustrator
Maimai108086 小时前
Web3 前端交易系统如何落地:从下单 UI 到 Operation 编码、签名与实时状态更新
前端·react.js·ui·架构·前端框架·web3
aqi007 小时前
一文速览 HarmonyOS 6.1.1 推出的十个新特性
android·华为·harmonyos·鸿蒙·harmony