鸿蒙聊天 Demo 练习 01:发送消息、模拟 AI 回复与自动滚动

鸿蒙聊天 Demo 练习 01:发送消息、模拟 AI 回复与自动滚动

一、本次分支

bash 复制代码
feature/chat-auto-scroll-loading

二、本次目标

本次在聊天 Demo 的基础页面上,完善了一个最小可用的聊天流程:

  1. 用户输入消息
  2. 点击发送
  3. 消息追加到聊天数组
  4. 模拟 AI 延迟回复
  5. 消息列表自动滚动到底部
  6. 发送期间禁用输入框和按钮,避免重复发送

三、涉及文件

text 复制代码
entry/src/main/ets/pages/Setting.ets

四、页面结构

当前聊天页面分为三部分:

text 复制代码
Column
├── Header       顶部标题栏
├── MessageList  中间消息列表
└── InputBar     底部输入栏

对应代码:

ts 复制代码
build() {
  Column() {
    this.Header()
    this.MessageList()
    this.InputBar()
  }
}

这种结构非常适合聊天页面:

  • 顶部固定标题
  • 中间区域使用 layoutWeight(1) 占据剩余空间
  • 底部输入框固定在页面底部

五、消息数据结构

ts 复制代码
interface ChatItem {
  id: number
  type: 'user' | 'ai'
  content: string
}

字段说明:

字段 作用
id 唯一标识,用于 ForEach 的 key
type 区分用户消息和 AI 消息
content 消息文本内容

六、为什么消息数组使用 concat

发送消息时使用:

ts 复制代码
this.chatList = this.chatList.concat([userItem])

而不是:

ts 复制代码
this.chatList.push(userItem)

原因是 concat 会返回一个新数组,更容易触发 ArkUI 的状态更新。

简单理解:

text 复制代码
push:修改原数组
concat:生成新数组,然后重新赋值

在响应式 UI 中,重新赋值通常更稳定。

七、Scroller 的作用

本次用到了:

ts 复制代码
private listScroller: Scroller = new Scroller()

然后绑定到 List

ts 复制代码
List({ scroller: this.listScroller }) {
  ...
}

这样就可以通过代码控制列表滚动:

ts 复制代码
this.listScroller.scrollToIndex(this.chatList.length)

八、为什么滚动到底部要用 setTimeout

消息数组更新后,UI 不一定立刻渲染完成。

如果马上滚动,可能会出现滚动不到最新消息的问题。

所以封装了一个方法:

ts 复制代码
scrollToBottom(): void {
  setTimeout(() => {
    this.listScroller.scrollToIndex(this.chatList.length)
  }, 50)
}

逻辑是:

text 复制代码
先更新数组
等待 UI 刷新
再滚动到底部

九、为什么滚动索引是 chatList.length

List 中除了消息列表,还有一个底部占位项:

ts 复制代码
ListItem() {
  Row() {
    Blank()
  }
  .height(12)
}

假设有 3 条消息:

text 复制代码
索引 0:第一条消息
索引 1:第二条消息
索引 2:第三条消息
索引 3:底部占位项

所以:

ts 复制代码
this.listScroller.scrollToIndex(this.chatList.length)

刚好可以滚到最后的底部占位项。

十、模拟 AI 回复

本次没有真正调用接口,而是使用 setTimeout 模拟异步请求:

ts 复制代码
mockAskAi(question: string): void {
  setTimeout(() => {
    const aiItem: ChatItem = {
      id: this.nextId++,
      type: 'ai',
      content: `你刚才说的是:${question}`
    }

    this.chatList = this.chatList.concat([aiItem])
    this.isSending = false
    this.scrollToBottom()
  }, 600)
}

以后真正接接口时,可以把 setTimeout 替换成网络请求。

十一、发送中状态

定义状态:

ts 复制代码
@Local isSending: boolean = false

发送时:

ts 复制代码
this.isSending = true

AI 回复完成后:

ts 复制代码
this.isSending = false

按钮根据状态变化:

ts 复制代码
Button(this.isSending ? '发送中' : '发送')
  .enabled(!this.isSending)

输入框也可以禁用:

ts 复制代码
.enabled(!this.isSending)

十二、本次知识点总结

本次练习涉及以下鸿蒙开发知识点:

  1. @ComponentV2 组件写法
  2. @Local 本地响应式状态
  3. Column / Row / List / ListItem / TextInput / Button
  4. ForEach 渲染数组
  5. Scroller 控制列表滚动
  6. scrollToIndex 滚动到指定位置
  7. setTimeout 处理 UI 渲染后的延迟滚动
  8. KeyboardAvoidMode.RESIZE 处理键盘顶起页面
  9. 使用 concat 更新数组,保证状态刷新
  10. 使用 isSending 控制按钮禁用和加载状态

十三、面试表达

这个功能可以这样说:

我在聊天 Demo 中实现了基础的消息发送和模拟 AI 回复流程。页面采用上中下布局,中间使用 List 渲染消息数组,底部使用 TextInputButton 处理输入。为了保证发送消息后能自动看到最新内容,我创建了 Scroller 实例并绑定到 List,在消息数组更新后通过 scrollToIndex 滚动到底部。同时考虑到 UI 渲染存在时序问题,所以封装了 scrollToBottom 方法,用 setTimeout 延迟滚动,保证列表刷新后再执行滚动。另外我还加入了 isSending 状态,模拟接口请求期间禁用输入和按钮,避免重复发送。

十四、本次提交命令

bash 复制代码
git add entry/src/main/ets/pages/Setting.ets docs/01-chat-auto-scroll-loading.md

git commit -m "feat: add chat auto scroll and loading state"

git push origin feature/chat-auto-scroll-loading

十五、本次练习总结

这一小节的重点不是聊天功能本身,而是理解一个聊天页面最基础的状态流转:

text 复制代码
用户输入
  ↓
点击发送
  ↓
追加用户消息
  ↓
模拟请求中状态
  ↓
追加 AI 回复
  ↓
滚动到底部

这套流程后面可以继续扩展成:

  • 打字机输出
  • 真实接口请求
  • 本地历史记录
  • 会话列表
  • 消息组件拆分
  • 输入框聚焦和键盘处理
相关推荐
lichenyang4535 小时前
把 demo 里的 console.log 全换成 HiLog:从 %{private} 没脱敏的困惑说起
前端
光影少年5 小时前
组件复用:HOC、Render Props、自定义Hook 对比
前端·react.js·掘金·金石计划
Gauss松鼠会6 小时前
【GaussDB】GaussDB SMP特性调优详解
java·服务器·前端·数据库·sql·算法·gaussdb
葬送的代码人生6 小时前
JavaScript 数组完全指南:从入门到实战
前端·javascript·算法
用户938515635076 小时前
深入理解 JavaScript 同步与异步:从单线程到事件循环与 Promise
前端·javascript
搬砖的码农6 小时前
造一个 Agent 运行时 #01:我决定开干,顺便把坑都写下来
前端·agent·ai编程
yingyima6 小时前
深入解析:定时任务失败重试机制的底层原理与实践
前端
哈撒Ki6 小时前
快速入门vue3与常见面试题
前端·vue.js·面试
踩着两条虫6 小时前
VTJ.PRO v2.4.2 私有化部署与升级实操指南
前端·人工智能·低代码·架构·数据挖掘
木斯佳6 小时前
前端八股文面经大全:美团前端暑期实习一面(2026-06-08)·面经深度解析
前端