HarmonyOS6 - 聊天页面实战案例

HarmonyOS6 - 聊天页面实战案例

开发环境为:

开发工具:DevEco Studio 6.0.1 Release

API版本是:API21

本文所有代码都已使用模拟器测试成功!

1. 效果图

我们需要实现下面这样效果图的聊天页面:

具体需求如下:

  1. 当输入框中输入数据时,发送按钮可点击,没有输入数据时,发送按钮置灰不可点击状态
  2. 输入内容后点击发送按钮,消息列表中底部会自动显发送消息内容
  3. 每次发送消息后,两秒钟后收到医生的固定回复内容:有事外出,请留言,我会尽快回复您的!
  4. 每次发送消息的时间需要显示系统当前时间
  5. 消息较多时,列表需要支持上下滑动查看消息数据

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的相关组件有一个更深入的理解和认识了,继续加油💪

相关推荐
IT陈图图16 小时前
基于 Flutter × OpenHarmony 的文本排序工具开发实战
flutter·开源·鸿蒙·openharmony
奋斗的小青年!!16 小时前
Flutter开发OpenHarmony应用:设置页面组件的深度实践
flutter·harmonyos·鸿蒙
ShiMetaPi17 小时前
八核RISC-V + 双屏输出 + 全接口扩展:M-K1HSE 深度解析
人工智能·机器人·鸿蒙·开源鸿蒙
世人万千丶20 小时前
鸿蒙跨端框架 Flutter 学习 Day 3:性能进阶——Iterable 延迟加载与计算流的智慧
学习·flutter·ui·华为·harmonyos·鸿蒙·鸿蒙系统
IT陈图图21 小时前
基于 Flutter × OpenHarmony 的个人中心— 主要设置内容区域实现解析
flutter·鸿蒙·openharmony
小白阿龙1 天前
鸿蒙+flutter 跨平台开发——Text控件
flutter·鸿蒙
世人万千丶1 天前
鸿蒙跨端框架 Flutter 学习 Day 3:综合实践——多维数据流与实时交互实验室
学习·flutter·华为·交互·harmonyos·鸿蒙
世人万千丶1 天前
鸿蒙跨端框架 Flutter 学习 Day 3:工程实践——数据模型化:从黑盒 Map 走向强类型 Class
学习·flutter·ui·华为·harmonyos·鸿蒙·鸿蒙系统
奋斗的小青年!!1 天前
Flutter跨平台开发鸿蒙应用的权限管理实践
flutter·harmonyos·鸿蒙