HarmonyOS6 - 聊天页面实战案例
开发环境为:
开发工具:DevEco Studio 6.0.1 Release
API版本是:API21
本文所有代码都已使用模拟器测试成功!
1. 效果图
我们需要实现下面这样效果图的聊天页面:

具体需求如下:
- 当输入框中输入数据时,发送按钮可点击,没有输入数据时,发送按钮置灰不可点击状态
- 输入内容后点击发送按钮,消息列表中底部会自动显发送消息内容
- 每次发送消息后,两秒钟后收到医生的固定回复内容:有事外出,请留言,我会尽快回复您的!
- 每次发送消息的时间需要显示系统当前时间
- 消息较多时,列表需要支持上下滑动查看消息数据
2. 思路分析
1. 数据层设计与初始化
- 定义消息数据模型(Message类),包含消息ID、发送者标识、头像、昵称、内容、时间等核心属性
- 设计消息数组存储结构,使用@State装饰器实现响应式数据管理
- 初始化模拟数据,构建医生和用户的对话记录,展示基础聊天场景
- 规划时间格式化函数,统一消息时间显示格式
2. 页面整体布局架构
- 采用Flex布局的Column容器,将页面划分为上、中、下三部分
- 顶部设计标题栏,居中显示"在线问诊",保持简洁统一的导航风格
- 中间区域使用Scroll组件实现可滚动聊天区域,关闭滚动条保持界面整洁
- 底部设计输入区域,包含表情按钮、文本输入框和发送按钮
- 使用layoutWeight属性实现自适应布局,确保聊天区域占据剩余空间
3. 消息展示与样式实现
- 区分左右消息布局:用户消息居右显示(绿色气泡),医生消息居左显示(白色气泡)
- 每条消息包含时间戳、头像、昵称和内容气泡,时间戳居中显示
- 使用borderRadius实现圆角气泡效果,左侧消息右上角尖角,右侧消息左上角尖角
- 头像采用圆形裁剪,昵称在医生消息中显示,用户消息不显示昵称
- 消息内容使用合适的字体大小和颜色,确保可读性
4. 交互功能实现
- 文本输入框实现双向绑定,实时同步用户输入内容
- 发送按钮状态管理:输入内容为空时禁用,有内容时启用并改变颜色
- 实现消息发送逻辑:创建新消息对象、添加到消息数组、清空输入框
- 集成Scroller控制器,发送消息后自动滚动到底部显示最新消息
- 模拟医生自动回复功能,2秒后发送预设回复内容
5. 性能与体验优化
- 使用Scroll组件替代List组件,简化实现同时保证流畅滚动
- 关闭滚动条显示,提升界面美观度
- 消息发送后立即触发滚动到底部,无需等待UI更新完成
- 采用相对时间戳显示,简化时间信息
- 输入区域固定高度,避免键盘弹出时的布局抖动
- 消息项使用合适的间距和边距,保证视觉层次清晰
3. 实战源码
新建ChatPage.ets文件,下面是源码:(已运行过,确定是可运行代码)
js
export class Message {
id: number = 0;
isMe: boolean = false; // 是否是自己发送的消息
avatar: string = ''; // 头像
nickname: string = ''; // 昵称
content: string = ''; // 消息内容
time: string = ''; // 时间
constructor(id: number, isMe: boolean, avatar: string, nickname: string, content: string, time: string) {
this.id = id;
this.isMe = isMe;
this.avatar = avatar;
this.nickname = nickname;
this.content = content;
this.time = time;
}
}
/**
* 聊天页面
*/
@Entry
@Component
struct ChatPage {
@State messageList: Array<Message> = [];
@State inputText: string = '';
private scroller: Scroller = new Scroller();
// 初始化消息数据
aboutToAppear() {
this.initMessages();
}
initMessages() {
this.messageList = [
new Message(1, false, '/images/doctor_avatar.png', '张医生', '您好,有什么可以帮助您的?', '10:30'),
new Message(2, true, '/images/my_avatar.png', '我', '医生您好,我最近有点咳嗽', '10:32'),
new Message(3, false, '/images/doctor_avatar.png', '张医生', '咳嗽多久了?有没有发烧?', '10:33'),
new Message(4, true, '/images/my_avatar.png', '我', '大概三天了,没有发烧,就是干咳', '10:35'),
new Message(5, false, '/images/doctor_avatar.png', '张医生',
'建议多喝水,注意休息,可以喝点蜂蜜水缓解', '10:36'),
new Message(6, true, '/images/my_avatar.png', '我', '好的,谢谢医生!', '10:37'),
new Message(7, false, '/images/doctor_avatar.png', '张医生', '不客气,如果症状加重请及时就医',
'10:38'),
];
}
// 发送消息
sendMessage() {
if (this.inputText.trim() === '') {
return;
}
const newMessage = new Message(
this.messageList.length + 1,
true,
'/images/my_avatar.png',
'我',
this.inputText,
this.getCurrentTime()
);
this.messageList.push(newMessage);
this.inputText = '';
// 模拟医生回复
setTimeout(() => {
const replyMessage = new Message(
this.messageList.length + 1,
false,
'/images/doctor_avatar.png',
'张医生',
'收到,现在忙,我会尽快回复您的!',
this.getCurrentTime()
);
this.messageList.push(replyMessage);
//滚动到底部
this.scroller.scrollEdge(Edge.Bottom);
}, 1000);
}
getCurrentTime(): string {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
build() {
Column({ space: 0 }) {
// 顶部标题栏
this.buildHeader()
// 中间聊天区域
this.buildChatArea()
// 底部输入区域
this.buildInputArea()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 构建顶部标题栏
@Builder
buildHeader() {
Row() {
Text('在线问诊')
.fontSize(22)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.padding({ bottom: 5 })
.backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center)
}
// 构建聊天区域
@Builder
buildChatArea() {
Scroll(this.scroller) {
Column() {
// 消息列表
ForEach(this.messageList, (message: Message) => {
this.buildMessageItem(message)
}, (message: Message) => message.id.toString())
}
.width('100%')
.padding({ bottom: 10 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off) //关闭滚动条
.layoutWeight(1) // 占据剩余空间
.backgroundColor('#F5F5F5')
.onReachEnd(() => {
// 滚动到底部的回调
})
}
// 构建单条消息
@Builder
buildMessageItem(message: Message) {
if (message.isMe) {
// 自己发送的消息(右侧显示)
Column() {
// 时间
Row() {
Text(message.time)
.fontSize(12)
.fontColor('#999999')
.alignSelf(ItemAlign.End)
}
.width('100%')
.justifyContent(FlexAlign.Center)
Row() {
// 消息内容
Row() {
Text(message.content)
.fontSize(16)
.fontColor('#FFFFFF')
.padding({
left: 12,
right: 12,
top: 8,
bottom: 8
})
.backgroundColor('#07C160')
.borderRadius({
topLeft: 10,
topRight: 2,
bottomLeft: 10,
bottomRight: 10
})
}
.margin({ right: 12, top: 8, bottom: 8 })
// 头像
Image(message.avatar)
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 12 })
}
.width('100%')
.justifyContent(FlexAlign.End)
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 12 })
} else {
// 对方发送的消息(左侧显示)
Column() {
// 时间
Row() {
Text(message.time)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.Center)
Row() {
// 头像
Image(message.avatar)
.width(40)
.height(40)
.borderRadius(20)
.margin({ left: 12 })
Column({ space: 4 }) {
// 昵称
Row() {
Text(message.nickname)
.fontSize(12)
.fontColor('#666666')
}
.width('100%')
// 消息内容
Row() {
Text(message.content)
.fontSize(16)
.fontColor('#000000')
.padding({
left: 12,
right: 12,
top: 8,
bottom: 8
})
.backgroundColor('#FFFFFF')
.borderRadius({
topLeft: 2,
topRight: 10,
bottomLeft: 10,
bottomRight: 10
})
}
.width('100%')
}
.margin({ left: 8, top: 8, bottom: 8 })
.width('79%')
}
.width('100%')
}
.width('100%')
.margin({ top: 12 })
}
}
// 构建底部输入区域
@Builder
buildInputArea() {
Row({ space: 8 }) {
// 表情按钮
Image($r('app.media.ic_emoji'))
.fillColor('#ff454545')
.width(28)
.height(28)
.margin({ left: 12 })
// 输入框
TextInput({ text: this.inputText, placeholder: '请输入消息...' })
.layoutWeight(1)
.height(40)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.padding({ left: 16, right: 16 })
.onChange((value: string) => {
this.inputText = value;
})
.onSubmit(() => {
this.sendMessage();
})
// 发送按钮
Button('发送')
.width(60)
.height(36)
.backgroundColor(this.inputText.trim() ? '#07C160' : '#CCCCCC')
.fontColor('#FFFFFF')
.fontSize(14)
.margin({ right: 12 })
.enabled(this.inputText.trim() !== '')
.onClick(() => {
this.sendMessage();
})
}
.width('100%')
.height(55)
.backgroundColor('#F0F0F0')
.padding({ top: 8, bottom: 8 })
.alignItems(VerticalAlign.Center)
}
}
需要在ets目录下新建images文件夹,将医生头像doctor_avatar.png和患者头像my_avatar.png存放进去即可
总结
经过本文的练习,相信大家对ArkTS的相关组件有一个更深入的理解和认识了,继续加油💪