这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

Vue3+TS(uniapp)手撸一个聊天页面

前言

最近在自己的小程序中做了一个智能客服,API使用的是云厂商的API,然后聊天页面...嗯,找了一下关于UniApp(vite/ts)版本的好像不多,有一个官方的但其中的其他代码太多了,去看懂再删除那些对我无用的代码不如自己手撸一个,先看效果:

好,下面开始介绍如何一步一步实现

重难点调研

1. 如何编写气泡

可以发现一般的气泡是有个“小箭头”,一般是指向用户的头像,所以这里我们的初步思路就是通过beforeafter伪类来放置这个小三角形,这个小三角形通过隐藏border的其余三边来实现。

然后其中一个细节就是聊天气泡的最大宽度不超过对方的头像,超过就换行。这个简单,设置一个max-width: cacl(100vw - XX)就可以了

2. 如何编写输入框

考虑到用户可能输入多行文字,这里使用的是<textarea>标签,点开微信发个消息试试,发现它是自适应的,这里去调研了解了一下,发现小程序自带组件有这个实现,好,那直接用:

然后我们继续注意到发送按钮与输入框的底线保持水平,这个flex里有对应属性可以实现,跳过...

3.如何实现滚动条始终居于底部

当聊天消息较多时,我们发现我们继续输入消息,页面并没有更新(滚动)。打开微信聊天框一看,当消息过多时,你发一条消息,页面就自动滚动到了最新的消息,这又是怎实现的呢?

继续调研,发现小程序自带的<scroll-view>标签中有个属性scroll-into-view可以自动跳转:

<scroll-view scroll-y="true" :scroll-into-view="`msg${messages.length-1}`" :scroll-with-animation="true">
<view class="msg-list" :id="`msg${index}`" v-for="(msg, index) in messages" :key="msg.time">
<view class="msg-item">

</view>
</view>
</scroll-view>

概述

简单分析下来好像一点都不难,如下是我的文件列表,话不多说,开始撸代码!

chat
├─ chat.vue
├─ leftBubble.vue
└─ rightBubble.vue

左气泡模块

左气泡模块就是刚刚分析的那一部分,然后增加一点点细节,如下:

<template>
<view class="left-bubble-container">
<view class="left">
<image :src="props.avatarUrl"></image>
</view>
<view class="right">
<view class="bubble">
<text>{{ props.message }}</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { userDefaultData } from "@/const"; interface propsI {
message: string;
avatarUrl: string;
} const props = withDefaults(defineProps<propsI>(), {
avatarUrl: userDefaultData.avatarUrl,
});
</script>
<style lang="scss" scoped>
.left-bubble-container {
margin: 10px 0;
display: flex;
.left {
image {
height: 50px;
width: 50px;
border-radius: 5px;
}
}
}
.bubble {
max-width: calc(100vw - 160px);
min-height: 25px;
border-radius: 10px;
background-color: #ffffff;
position: relative;
margin-left: 20px;
padding: 15px;
text {
height: 25px;
line-height: 25px;
}
}
.bubble::before {
position: absolute;
top: 15px;
left: -20px;
content: "";
width: 0;
height: 0;
border-right: 10px solid #ffffff;
border-bottom: 10px solid transparent;
border-left: 10px solid transparent;
border-top: 10px solid transparent;
}
</style>

右气泡模块

右气泡模块我们需要将三角形放在右边,这个好实现。然后这整个气泡我们需要让它处于水平居右,所以这里我使用了:

display: flex;
direction: rtl;

这个属性,但使用的过程中发现气泡中的内容(符号与文字)会出现翻转,“遇事不决,再加一层”,所以我们在内容节点外再套一层:

<span style="direction: ltr; unicode-bidi: bidi-override">
<text>{{ props.message }}</text>
</span>

然后继续增加一点点细节:

<template>
<view class="left-bubble-container">
<view class="right">
<image :src="props.avatarUrl"></image>
</view>
<view class="left">
<view class="bubble">
<span style="direction: ltr; unicode-bidi: bidi-override">
<text>{{ props.message }}</text>
</span>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { userDefaultData } from "@/const"; interface propsI {
message: string;
avatarUrl: string;
} const props = withDefaults(defineProps<propsI>(), {
avatarUrl: userDefaultData.avatarUrl,
});
</script>
<style lang="scss" scoped>
.left-bubble-container {
display: flex;
direction: rtl;
margin: 10px 0;
.right {
image {
height: 50px;
width: 50px;
border-radius: 5px;
}
}
}
.bubble {
max-width: calc(100vw - 160px);
min-height: 25px;
border-radius: 10px;
background-color: #ffffff;
position: relative;
margin-right: 20px;
padding: 15px;
text-align: left;
text {
height: 25px;
line-height: 25px;
}
}
.bubble::after {
position: absolute;
top: 15px;
right: -20px;
content: "";
width: 0;
height: 0;
border-right: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 10px solid #ffffff;
border-top: 10px solid transparent;
}
</style>

输入模块

没啥说的,需要注意的是:Button记得防抖

<view class="bottom-input">
<view class="textarea-container">
<textarea
auto-height
fixed="true"
confirm-type="send"
v-model="input"
@confirm="submit"
/>
</view>
<button
style="
width: 70px;
height: 40px;
line-height: 34px;
margin: 0 10px;
background-color: #ffffff;
border: 3px solid #0256ff;
color: #0256ff;
"
@click="submit"
>
发送
</button>

整体

1)考虑如何存储消息

这里仅考虑内存中如何存储,不考虑本地存储,后续思考中会聊到。

export interface messagesI {
left: boolean;
text: string;
time: number;
}

如上是消息列表中的一项,为了区分是渲染到左气泡还是右气泡,这里用left来区分了一下;

const messages: Ref<messagesI[]> = ref([]);

2)如何推荐消息

这边我封装的服务端接口是这样的:

mutation chat{
customerChat(talk: "你好啊"){
knowledge
text
recommend
}
}

recommend是用户可能输入了错误的消息,这里是预测用户的输入字符串,所以我们需要在得到这个字符串后直接显示,然后用户可以一键通过这条消息回复:

function submit(){
// 略...
const finalMsg = receive?.knowledge || receive?.text || "你是否想问: " + receive?.recommend;
// 略...
if (receive?.recommend) {
input.value = receive?.recommend;
} else {
input.value = "";
}
}

如上,得益于Vue框架,这里实现起来也非常简单,当用户提交之后,如果有推荐的消息,就直接修改input.value从而修改输入框的文字;如果没有就直接清空方便下一次输入。

接下来继续增加一点点细节(chat.vue文件)

<template>
<view class="chat-container">
<view class="msg-container">
<!-- https://github.com/wepyjs/wepy-wechat-demo/issues/7 -->
<scroll-view scroll-y="true" :scroll-into-view="`msg${messages.length-1}`" :scroll-with-animation="true">
<view class="msg-list" :id="`msg${index}`" v-for="(msg, index) in messages" :key="msg.time">
<view class="msg-item">
<left-bubble v-if="msg.left" :message="msg.text" :avatar-url="meStore.user?.avatarUrl"></left-bubble>
<right-bubble v-else :message="msg.text" :avatar-url="logoUrl"></right-bubble>
</view>
</view>
</scroll-view>
</view>
<view class="bottom-input">
<view class="textarea-container">
<textarea
auto-height
fixed="true"
confirm-type="send"
v-model="input"
@confirm="submit"
/>
</view>
<button
style="
width: 70px;
height: 40px;
line-height: 34px;
margin: 0 10px;
background-color: #ffffff;
border: 3px solid #0256ff;
color: #0256ff;
"
@click="submit"
>
发送
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, type Ref } from "vue";
import leftBubble from "./leftBubble.vue";
import rightBubble from "./rightBubble.vue";
import type { messagesI } from "./chat.interface";
import { chatGQL } from "@/graphql/me.graphql";
import { useMutation } from "villus";
import { logoUrl } from "@/const";
import { useMeStore } from "@/stores/me.store"; const meStore = useMeStore(); const messages: Ref<messagesI[]> = ref([]);
const input = ref(""); async function submit() {
if (input.value === "") return;
messages.value.push({
left: true,
text: input.value,
time: new Date().getTime(),
});
const { execute } = useMutation(chatGQL);
const { error, data } = await execute({ talk: input.value })
if (error) {
uni.showToast({
title: `加载错误`,
icon: "error",
duration: 3000,
});
throw new Error(`加载错误: ${error}`);
}
const receive = data?.customerChat;
const finalMsg = receive?.knowledge || receive?.text || "你是否想问: " + receive?.recommend;
messages.value.push({
left: false,
text: finalMsg,
time: new Date().getTime(),
});
if (receive?.recommend) {
input.value = receive?.recommend;
} else {
input.value = "";
}
} </script>
<style lang="scss" scoped>
.chat-container {
.msg-container {
padding: 20px 5px 100px 5px;
height: calc(100vh - 120px);
scroll-view {
height: 100%;
}
}
.bottom-input {
display: flex;
align-items: flex-end;
position: fixed;
bottom: 0px;
background-color: #fbfbfb;
padding: 20px;
box-shadow: 0px -10px 30px #eeeeee;
.textarea-container {
background-color: #ffffff;
padding: 10px;
textarea {
width: calc(100vw - 146px);
background-color: #ffffff;
}
}
}
}
</style>

思考

如何保存到本地,然后每次加载最新消息,然后向上滚动进行懒加载?

我这里没有实现该功能,毕竟只是一个客服,前端没必要保存消息记录到本地如Localstorage。

这里抛砖引玉,想到了一个最基础的数据结构--链表,用Localstorage-key/value的形式来实现消息队列在本地的多段存储:

当然,有效性有待验证,这里仅仅属于一些想法

最后

然后,我撸了小半天的页面,准备给朋友看看来着,他告诉我微信小程序自带一个客服系统,只需要让buttonopen-type属性等于contract

本文转载于:

https://juejin.cn/post/7224059698911641658

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--Vue3+TS(uniapp)手撸一个聊天页面的更多相关文章

  1. 使用Java Socket手撸一个http服务器

    原文连接:使用Java Socket手撸一个http服务器 作为一个java后端,提供http服务可以说是基本技能之一了,但是你真的了解http协议么?你知道知道如何手撸一个http服务器么?tomc ...

  2. 手撸一个SpringBoot-Starter

    1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...

  3. Golang:手撸一个支持六种级别的日志库

    Golang标准日志库提供的日志输出方法有Print.Fatal.Panic等,没有常见的Debug.Info.Error等日志级别,用起来不太顺手.这篇文章就来手撸一个自己的日志库,可以记录不同级别 ...

  4. 【手撸一个ORM】MyOrm的使用说明

    [手撸一个ORM]第一步.约定和实体描述 [手撸一个ORM]第二步.封装实体描述和实体属性描述 [手撸一个ORM]第三步.SQL语句构造器和SqlParameter封装 [手撸一个ORM]第四步.Ex ...

  5. 第二篇-用Flutter手撸一个抖音国内版,看看有多炫

    前言 继上一篇使用Flutter开发的抖音国际版 后再次撸一个国内版抖音,大部分功能已完成,主要是Flutter开发APP速度很爽,  先看下图 项目主要结构介绍 这次主要的改动在api.dart 及 ...

  6. 通过 Netty、ZooKeeper 手撸一个 RPC 服务

    说明 项目链接 微服务框架都包括什么? 如何实现 RPC 远程调用? 开源 RPC 框架 限定语言 跨语言 RPC 框架 本地 Docker 搭建 ZooKeeper 下载镜像 启动容器 查看容器日志 ...

  7. C#基于Mongo的官方驱动手撸一个Super简易版MongoDB-ORM框架

    C#基于Mongo的官方驱动手撸一个简易版MongoDB-ORM框架 如题,在GitHub上找了一圈想找一个MongoDB的的ORM框架,未偿所愿,就去翻了翻官网(https://docs.mongo ...

  8. 手撸一个springsecurity,了解一下security原理

    手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...

  9. 五分钟,手撸一个Spring容器!

    大家好,我是老三,Spring是我们最常用的开源框架,经过多年发展,Spring已经发展成枝繁叶茂的大树,让我们难以窥其全貌. 这节,我们回归Spring的本质,五分钟手撸一个Spring容器,揭开S ...

  10. 以鶸ice为例,手撸一个解释器(一)明确目标

    代码地址 # HelloWorld.ice print("hello, world") 前言(废话) 其实从开始学习编译原理到现在已经有快半年的时间了,但是其间常常不能坚持看下去龙 ...

随机推荐

  1. Matplotlib绘制散点图与条形图

    Matplotlib绘制散点图与条形图 绘制散点图 # 绘制散点图 from matplotlib import pyplot as plt from matplotlib import font_m ...

  2. 思维分析逻辑 4 DAY

    目录 竞品分析 波特五力模型 竞品分析步骤 分析目的 对比分析 初步结论 活动营销分析 用户增长分析 用户增长基本模型 渠道思维(前期) 用户思维(中期) ROI思维(后期) 增长思维 北极星指标:一 ...

  3. JS Leetcode 304. 二维区域和检索 - 矩阵不可变,彻底弄懂二维数组前缀和

    壹 ❀ 引 我在JS LeetCode 303. 区域和检索 - 数组不可变,一维数组的前缀和一文中,记录了一维数组求区间合的解题思路,正好还有一题的升级版,题目来自leetcode304. 二维区域 ...

  4. Goland 使用[临时]

    环境变量 因为module模式的引入, 多个项目可以共用同一套External Libraries, 在File->Settings->Go中, 设置GOROOT为go安装目录, 例如 / ...

  5. 【OpenGL ES】正方形图片贴到圆形上

    1 前言 ​ 纹理贴图 中介绍了将矩形图片贴到矩形模型上,本文将介绍:在不裁剪图片的情况下,将正方形的图片贴到圆形模型上. ​ 思考:实数区间 [0, 1] 与 [0, 2] 的元素可以建立一一映射关 ...

  6. mc命令

    mc命令 mc是一个基于字符的目录浏览器和文件管理器,其将熟悉的图形文件管理器和常见的命令行工具联系在一起,mc的设计基于文件管理器中双目录窗格的设计,其中同时显示两个目录的列表,可以执行所有常见的文 ...

  7. Oracle 高水位(HWM: High Water Mark) 说明

    一. 准备知识:ORACLE的逻辑存储管理. ORACLE在逻辑存储上分4个粒度: 表空间, 段, 区 和 块. 1.1 块: 是粒度最小的存储单位,现在标准的块大小是8K,ORACLE每一次I/O操 ...

  8. Spring源码阅读系列--全局目录

    阅读之前要注意的东西:本文就是主打流水账式的源码阅读,主导的是一个参考,主要内容需要看官自己去源码中验证.全系列文章基于 spring 源码 5.x 版本. 写在开始前的话: 阅读spring 源码实 ...

  9. C++ 多线程的错误和如何避免(1)

    在终止程序之前没有使用 join() 等待后台线程 前提分析:线程分为 joinable  状态和 detached 状态 添加 .join() 这句代码的时候,就表示主线程需要等待子线程运行结束回收 ...

  10. Lua学习笔记之迭代器、table、模块和包、元表和协程

    迭代器 迭代器是一种对象,它能够来遍历标准库模板容器中的部分或全部元素,每个迭代器对象代表容器中确定的地址,在Lua中迭代器是一种支持指针类型的结构,他可以遍历集合的每一个元素. 泛型for迭代器 泛 ...