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

news2024/11/20 16:29:24

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

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

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/516488.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux常用命令——ifconfig命令

在线Linux命令查询工具 ifconfig 配置和显示Linux系统网卡的网络参数 补充说明 ifconfig命令被用于配置和显示Linux内核中网络接口的网络参数。用ifconfig命令配置的网卡信息&#xff0c;在网卡重启后机器重启后&#xff0c;配置就不存在。要想将上述的配置信息永远的存的电…

如何用Facebook为你的跨境电商店铺进行引流?

对于跨境电商店铺来说&#xff0c;引流客户代表着潜在的商业机会和利润。当更多潜在客户访问你的店铺并下单购买产品时&#xff0c;这将增加你的销售额和利润&#xff0c;并帮助你的品牌影响力不断扩大。 一、Facebook广告付费玩法 1.创建一个Facebook商业页面&#xff1a;首先…

首发 | 人工智能赋能的未来作战构想(上): 海上作战篇

源自&#xff1a;中国指挥与控制学会 ▲图1&#xff1a;报告封面和封底 一、人工智能赋能改变战争形态 ▲图2&#xff1a;以AI赋能万物互联 二、人工智能赋能的海上作战 ▲图3&#xff1a;海上作战要保持持续有效的火力输出 &#xff08;一&#xff09;海上防御作战构想 ▲图4&…

MODBUS协议下,触摸屏与PLC能否实现无线通讯?

想要实现触摸屏与PLC之间的MODBUS无线通讯&#xff0c;就需要一个满足这两个设备通讯接口&#xff0c;并可在MODBUS协议下运行的通讯终端作为媒介&#xff0c;搭建起这两者之间的通讯桥梁。如果是使用威纶通触摸屏作为主站&#xff0c;三菱FX5U PLC作为从站&#xff0c;便可采用…

代码随想录-数组

学习《代码随想录》 理论基础什么是数组&#xff1f; 二分查找左闭右闭左闭右开 移除元素暴力法双指针法 长度最小的子数组暴力法滑动窗口 螺旋矩阵 理论基础 什么是数组&#xff1f; 数组是存储在连续内存空间上的相同类型数据的集合。 二分查找 有两种解法&#xff1a;左闭…

spring6概述

Spring6要求JDK最低版本是JDK17 Spring是什么&#xff1f; Spring 是一款主流的 Java EE 轻量级开源框架 &#xff0c;Spring 由“Spring 之父”Rod Johnson 提出并创立&#xff0c;其目的是用于简化 Java 企业级应用的开发难度和开发周期。Spring的用途不仅限于服务器端的开…

阿里云李钟:弹性计算控制系统团队的提效之路

2023 年 3 月 25 日&#xff0c;“城市领航之夜第一期”活动在上海举行&#xff0c;阿里云弹性计算控制系统技术架构负责人李钟出席了本次活动并带来了《弹性计算控制系统团队提效之路》的主题演讲&#xff0c;为大家详细分享了阿里云弹性计算控制系统团队所面临的挑战、如何通…

计算机毕业论文选题推荐|软件工程|系列五

文章目录 导文题目导文 计算机毕业论文选题推荐|软件工程 (***语言)==使用其他任何编程语言 例如:基于(***语言)门窗账务管理系统的设计与实现 得到:基于JAVA门窗账务管理系统的设计与实现 基于vue门窗账务管理系统的设计与实现 等等 题目 基于(***语言)遥感图像的居民…

言之画: AI绘画平台

【产品介绍】 言之画是出门问问推出的AI绘画平台。支持二次元、蒸汽朋克、插画等 8 种创作风格。用户只需输入文字&#xff0c;「言之画」就能一次性生成 8 张光影逼真、细节丰富的 2K 高分辨率图像。 除了以文生图&#xff0c;它还拥有以图生图、动图生成、个性头像生成等 AI …

MATLAB 之 常用内部函数,运算,字符串和结构数据与单元数据

文章目录 一、常用内部函数1. 常用数学函数2. 矩阵的超越函数2.1 矩阵平方根2.2 矩阵对数2.3 矩阵指数2.4 普通矩阵函数 二、MATLAB 运算1. 算术运算1.1 基本算术运算1.2 点运算 2. 关系运算3. 逻辑运算 三、字符串1. 字符串的表示2. 字符串的操作2.1 字符串的执行2.2 字符串与…

ChatGPT下的自动化工具Auto-GPT、AgentGPT、BabyAGI和GodMode

ChatGPT 对于一个大目标来说&#xff0c;他是不知道怎么一步一步的帮我们构建出来。有了Auto-GPT、AgentGPT、BabyAGI和GodMode 这些自动化工具&#xff0c;我们只需要输入一个大目标&#xff0c;这些自动化工具能够帮我们一步一步轻松实现。 1.Auto-GPT 网址&#xff1a;htt…

C/C++每日一练(20230512) 成绩打印、补齐数组、水仙花数

目录 1. 成绩打印 ※ 2. 按要求补齐数组 &#x1f31f;&#x1f31f;&#x1f31f; 3. 水仙花数 ※ &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 成绩打印 一个班有10个同学&am…

airserver7.2.7最新中文版下载及功能介绍

最近开会打算把手机投屏到自己的Mac上演示用&#xff0c;于是就打算用下听了很久好用但是一值没有使用的AirServer!十分简单的操作就可以完美的投屏到Mac电脑&#xff0c;而且不用像Mac自带的QuickTime用线连接手机!它可以把AirPlay / AirTunes上的音频、视频、照片、幻灯片还有…

母亲节祝福html源码,母亲节祝福html代码,母亲节表白妈妈代码,母亲节祝福网页代码

母亲节祝福html源码&#xff0c;母亲节祝福html代码&#xff0c;母亲节表白妈妈代码&#xff0c;母亲节祝福网页代码 运行截图 核心代码&#xff1a; <!DOCTYPE HTML> <html lang"zh-Hans"> <head><meta charset"utf-8"><meta…

我们选择护眼台灯的标准是什么?护眼台灯的国家标准

小时候在学校&#xff0c;父母跟老师经常强调“眼睛是心灵的窗户”要保护好眼睛&#xff0c;不管是在学校上学还是在家写作业时&#xff0c;都会很突然的听见“头抬高点”、“眼睛离远点”等类似的劝导&#xff0c;刚开始还不以为然&#xff0c;直到视力下降&#xff0c;带上了…

ctf_BUUCTF_web_总结(待更新)

文章目录 SQL注入文件上传漏洞文件包含漏洞php反序列化RCE模板安全问题&#xff08;SSTI&#xff09;XXE代码审计杂七杂八的知识点正则表达式CTF做题总结 SQL注入 [极客大挑战 2019]EasySQL[SUCTF 2019]EasySQL[强网杯 2019]随便注[极客大挑战 2019]BabySQL[BJDCTF2020]Easy MD…

雪浪云算力开发者大赛

1赛题背景 汽车生产工艺复杂&#xff0c;一辆汽车的制造需要完成焊装、涂装、总装三大工艺&#xff0c; 经过焊装车间、涂装车间、总装车间。各车间存在上下游关联关系&#xff0c;每个车间有自己的优化排序目标&#xff0c;需要综合考虑多种复杂的排序规则及工艺约束&#xf…

9.100ASK_V853-PRO开发板支持E907小核开发

0.前言 ​ 100ASK_V853-PRO开发板的V853芯片集成Arm Cortex-A7和RISC-V E907 双CPU&#xff0c;玄铁E907 是一款完全可综合的高端 MCU 处理器。它兼容 RV32IMAC 指令集&#xff0c;提供可观的整型性能提升以及高能效的浮点性能。E907 的主要特性包括&#xff1a;单双精度浮点单…

centos7设置时区,时间+时间同步的三种方式

centos7设置时区&#xff0c;时间时间同步的三种方式 1.centos7设置时区 1.1查看当前时区&#xff1a; timedatectl 1.2查看时间命令&#xff1a; date1.3选择时区命令 [rootlocalhost test]# tzselect Please identify a location so that time zone rules can be set co…

第五十六章 Unity 音频播放

Unity可以导入大多数标准音频文件格式&#xff0c;精通于在3D 空间中播放声音&#xff0c;还可根据需要提供其他效果。虽然播放声音是一件非常简单的事情&#xff0c;但是为了模拟现实直接中的各种声音效果&#xff0c;Unity会提供各种各样的组件来实现。 首先&#xff0c;我们…