uniapp - AI 聊天页面布局的实现

一、前言

这个 AI 聊天涉及的东西还是比较多的,接口请求、页面布局、组件封装、点击交互等等。我想我得分几篇文章来说明,这篇文章主要是针对这个布局的实现。

二、布局分析

这是总体的UI布局,大致可以分为 顶部显示栏、中间内容区域和底部键盘编辑区。

这个页面可初步这么设计,考虑到不同尺寸的手机长度不一样,加上当键盘弹起时,需要给外层元素一个 padding-bottom, 因此中间的内容区域的高度固定则显得不太合理,所以使用 flex 布局,中间内容区域占据剩余宽度。

html 复制代码
<template>
  <view class="page">
    <u-header />
    <view class="content"></view>
    <u-keyboard @send="onSend"></u-keyboard>
  </view>
</template>
scss 复制代码
.page {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  background-color: #f4f5f7;
    .content{
      flex: 1;
    }
  }

三、布局交互

这才是 AI聊天页面中的重要部分。 content 的高度表现为一个固定值,在交互上有几点需要考虑。

  • 滚动:在 content 区域内是要能够上下滚动的。
  • 消息插入:最初消息是从顶部挨个插入的,当填满整个 content 区域后,消息从底部插入,向上推动其他内容,最新的一条保持在区域的底部(参考如微信等聊天页面)
  • 历史记录:历史记录是从上面加载的,向下滑动屏幕,平滑加载聊天记录。

1. 滚动

普通元素也能滚动,但为了贴合小程序和使用一些API,则初步确定了使用 scroll-view 来实现。那我的实现是这样的, content 是一个容器,其中子元素为 scroll-view, 消息在 scroll-view 滚动。

html 复制代码
<template>
  <view class="page">
    <u-header />
    <view class="content">
       <scroll-view class="chat-list">
           <view class="chat-inner"></view>
       </scroll-view>
    </view>
    <u-keyboard @send="onSend"></u-keyboard>
  </view>
</template>
scss 复制代码
.page {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  background-color: #f4f5f7;
  .content {
    box-sizing: border-box;
    width: 100vw;
    flex: 1;
    overflow: auto;
    .chat-list {
      box-sizing: border-box;
      height: 100%;
      .chat-inner {
        min-height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        padding: 0 30rpx;
      }
    }
  }
}

当消息内容很多的时候,.chat-inner 即被撑开,在 .chat-list 里滚动,同时滚动到顶部/底部(content的顶部/底部)的时候,能触发 scroll-view 的方法。

2. 消息插入

这是我摸索时间最长的一个模块。在默认情况下,消息能从顶部依次往下插入,但当消息到达 content 底部时,最新的消息并不会往上推动其他消息。

使用 数组 渲染消息,ai回复的消息在左边,用户发送的消息在右边。当发送消息时,向 数组 插入一条消息。

所以,我们最直观的想法是 手动 移动页面,正好 scroll-view 有提供这样的方法,scroll-top 设置滚动条的位置。可以简单理解 scroll-top:0 ,就会滚动到顶部。那要滚动到底部,只需要设置足够大的值就可以。所以在最初的时候,当插入消息时,就设置:scroll-top:99999999999999。确实,在最初的逻辑交互中,这种方式能实现目标。

2.1 用户消息

以上的方法就能满足用户消息插入的交互,这里打字机效果了,用户消息没有此效果,因此是突然移动页面。

2.2 AI 消息

简单的打字机效果,当识别到是同一种消息类型时,就想文本进行拼接。如第一条:"今天",第二条:"是星期三",则把这两条拼接起来,然后使用定时器输出内容(这在后面的md组件中再详细说明)

AI 消息是流式返回的,当然在接收到流式消息后,前端需要将消息进行拼接,然后使用定时器均匀的输出文本。此时就很难在接收消息的时候设置 scroll-top:99999999999999 。比如流式返回三条数据,那就要设置三次,但此时页面上的消息还没渲染出来,哪怕是渲染出来了,也是一个字一个字的出现。

我当时的思路是,在AI返回的消息中,添加时间戳为这条消息的ID,即id: Date.now()。在ai消息打字输出的时候,调用方法设置 scroll-top 的值为这条消息的 ID。

正如我所想,该方法确实能实现,AI 消息刚冒头时,content 还是停留在之前的位置,随着内容打字输出,content慢慢向上移动,保持该条消息处于底部。

3. 历史记录

这个需要触顶操作,当滚动到顶部时,会触发 scroll-view 的方法。在这个方法里,获取一定的历史数据,向数组前面插入这些数据。这里元素有个默认行为,插入元素后,页面会直接 跳到顶部,这并不符合我们的预期。

同样是最直观的想法,把之前第一条消息的id记录下来,当插入更多数据后,重新设置 scroll-top 的值为记录的id,则页面停留在之前加载更多的位置。

很遗憾,页面确实能停留,但只能说是"附近",而且页面会跳动一下,体验并不是很好,与一些聊天软件相比,出入非常大。 再次查看文档,注意到这个属性 scroll-into-view,也是动手尝试了。

scroll-into-view: 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素

通过这个介绍,无疑是完美的,但是尝试了之后,依然是存在跳动的现象。

在开发过程中,借助了 ChatGpt 、DeepSeek 、cursor、豆包 、 通义千问、Trae 等ai工具,大差不差地都是推荐 scroll-top,随后又推荐 scroll-into-view。 对于闪动问题,提到了页面重新渲染导致,使用 nextTick 处理。但结果都是,无法解决。

四、不一样的灵感

目前的难点都是消息插入与定位,消息从底部平滑插入,历史记录在顶部平滑加载。平常的情况与元素默认行为是,向页面插入内容时,能从顶部平滑插入。底部加载更多时,页面不会跳动,能平滑加载。

等等,灵感来了,那此时把手机倒过来,默认的行为岂不就是我们需要的效果了吗?

因此,尝试着什么都反着来,将页面先翻转180°,插入消息,从原来的 push 变为 unshift。历史记录,从原来的插入前面,变成插入后面。

scss 复制代码
.page {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  background-color: #f4f5f7;
  .content {
    box-sizing: border-box;
    width: 100vw;
    flex: 1;
    overflow: auto;
    .chat-list {
      box-sizing: border-box;
      height: 100%;
      transform: rotateX(-180deg);  /* scroll-view 翻转 */
      .chat-inner {
        min-height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: flex-end; /* 最初的消息出现在顶部 */
        padding: 0 30rpx;
      }
    }
    .message-wrapper {
      box-sizing: border-box;
      display: flex;
      transform: rotateX(-180deg);  /* 将消息盒子 回正 */
    }
    .from-user {
      justify-content: flex-end;
      margin: 30rpx 0;
    }
    .from-ai {
      justify-content: flex-start;
    }
  }
}

到此,页面的布局可以说是完美了,不需要什么 scroll-into-viewscroll-top,不需要动态设置什么位置,全靠默认行为,消息一插入就能出现在底部。加载更多消息时,实际上是使用 @scrolltolower 进行触底加载,能平滑加载。

html 复制代码
<template>
  <view class="page">
    <u-header />
    <view class="content">
       <scroll-view class="chat-list"  @scrolltolower="handleScrollTolower">
           <view class="chat-inner">
              <view class="['message-wrapper',item.role === 'user' ? 'from-user' : 'from-ai]"
                v-for="(item, index) in messages"
                :key="index"
                :id="item.id">
              </view>
           </view>
       </scroll-view>
    </view>
    <u-keyboard @send="onSend"></u-keyboard>
  </view>
</template>
相关推荐
paopaokaka_luck4 分钟前
基于SpringBoot+Uniapp球场预约小程序(腾讯地图API、Echarts图形化分析、二维码识别)
spring boot·小程序·uni-app
于慨42 分钟前
uniapp各端通过webview实现互相通信
uni-app
一只小风华~1 小时前
Web前端:JavaScript和CSS实现的基础登录验证功能
前端
90后的晨仔1 小时前
Vue Router 入门指南:从零开始实现前端路由管理
前端·vue.js
LotteChar1 小时前
WebStorm vs VSCode:前端圈的「豆腐脑甜咸之争」
前端·vscode·webstorm
90后的晨仔1 小时前
零基础快速搭建 Vue 3 开发环境(附官方推荐方法)
前端·vue.js
洛_尘1 小时前
Java EE进阶2:前端 HTML+CSS+JavaScript
java·前端·java-ee
孤独的根号_2 小时前
Vite背后的技术原理🚀:为什么选择Vite作为你的前端构建工具💥
前端·vue.js·vite
吹牛不交税2 小时前
Axure RP Extension for Chrome插件安装使用
前端·chrome·axure
薛定谔的算法2 小时前
# 前端路由进化史:从白屏到丝滑体验的技术突围
前端·react.js·前端框架