最近用vue3 + js 写了聊天机器人页面 总结了几个问题
实现的功能 : 发送消息后会显示时间、可复制、可删除 ;
输入框内容太多时会固定在一个高度内 ,有滚动条方便编辑查看 ;
输入框一键清空;
发送多条信息后 , 视角会自动跳转到最后一条信息 ,不必再手动滑动
最终效果如下
----------------------------------------------------------------------------------------------
问题1 : 输入框内容太多时 , 没有固定一个高度 , 还会遮挡已有的信息
<div class="chat-input">
<el-input
v-model="input"
@keyup.enter.prevent="sendMessage"
placeholder="试着输入点什么..."
class="input"
type="textarea"
:rows="3"
autosize
:style="{ maxHeight: '100px', overflowY: 'auto' }" //加上这部分可解决问题
>
</el-input>
<div class="clear" @click="clearInput">
<img src="../../assets/image/清空.png" alt="" />
<div class="text">清空输入框</div>
</div>
<el-button
class="button"
type="primary"
@click="sendMessage"
:icon="Position"
>发送</el-button
>
</div>
问题2 : 回车键有抖动效果
<div class="chat-input">
<el-input
v-model="input"
@keyup.enter.prevent="sendMessage" //改为@keydown,@keyup和enter可能会冲突
placeholder="试着输入点什么..."
class="input"
type="textarea"
:rows="3"
autosize
:style="{ maxHeight: '100px', overflowY: 'auto' }"
>
</el-input>
<div class="clear" @click="clearInput">
<img src="../../assets/image/清空.png" alt="" />
<div class="text">清空输入框</div>
</div>
<el-button
class="button"
type="primary"
@click="sendMessage"
:icon="Position"
>发送</el-button
>
</div>
问题3 : 发送消息后不能跳到指定位置
<el-scrollbar height="660px">
<div class="chat">
<div class="chat-messages">
<div
v-for="(item, index) in messages"
:key="item.id"
class="message"
>
<div class="content">{{ item.text }}</div>
<div class="date">{{ formatTimestamp(item.timestamp) }}</div>
<div class="delate" @click="delateMsg(index)">
<img src="../../assets/image/删除.png" alt="" />
<div class="text">删除</div>
</div>
<div class="copy" @click="copyMsg(index)">
<img src="../../assets/image/复制.png" alt="" />
<div class="text">复制</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div class="chat-input">
<el-input
v-model="input"
@keyup.enter.prevent="sendMessage"
placeholder="试着输入点什么..."
class="input"
type="textarea"
:rows="3"
autosize
:style="{ maxHeight: '100px', overflowY: 'auto' }"
>
</el-input>
<div class="clear" @click="clearInput">
<img src="../../assets/image/清空.png" alt="" />
<div class="text">清空输入框</div>
</div>
<el-button
class="button"
type="primary"
@click="sendMessage"
:icon="Position"
>发送</el-button
>
</div>
<script setup>
import { ref, nextTick } from 'vue'
const messages = ref([])
const input = ref('')
const text = ref('')
const sendMessage = async () => { //声明异步函数,为方便下面使用await nextTick()
if (input.value.trim() !== '') {
messages.value.push({
id: Date.now(),
text: input.value,
timestamp: new Date()
})
input.value = ''
await nextTick() // 等待DOM更新完成
const messageElements = document.getElementsByClassName('message')
const lastMessageElement = messageElements[messageElements.length - 1] // 找到最后一个信息类名
lastMessageElement.scrollIntoView({ behavior: 'smooth', block: 'end' }) // 平滑的跳转到最后一个信息
}
}
完整代码如下
<template>
<div class="deploy">
<div class="cardTop">开始聊天吧</div>
<el-card class="elCard">
<div class="left">
<ul class="leftUl">
<li class="leftLi">
<div class="circle">
<img src="../../assets/image/圆圈.svg" alt="" />
</div>
</li>
<li class="leftLi2">
<div class="circle">
<img src="../../assets/image/圆圈.svg" alt="" />
</div>
</li>
</ul>
<div class="robot">
<img src="../../assets/image/机器人.png" alt="" />
<img src="../../assets/image/信息2.png" alt="" />
</div>
</div>
<div class="right">
<el-scrollbar height="660px">
<div class="chat">
<div class="chat-messages">
<div
v-for="(item, index) in messages"
:key="item.id"
class="message"
>
<div class="content">{{ item.text }}</div>
<div class="date">{{ formatTimestamp(item.timestamp) }}</div>
<div class="delate" @click="delateMsg(index)">
<img src="../../assets/image/删除.png" alt="" />
<div class="text">删除</div>
</div>
<div class="copy" @click="copyMsg(index)">
<img src="../../assets/image/复制.png" alt="" />
<div class="text">复制</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div class="chat-input">
<el-input
v-model="input"
@keydown.enter.prevent="sendMessage"
placeholder="试着输入点什么..."
class="input"
type="textarea"
:rows="3"
autosize
:style="{ maxHeight: '100px', overflowY: 'auto' }"
>
</el-input>
<div class="clear" @click="clearInput">
<img src="../../assets/image/清空.png" alt="" />
<div class="text">清空输入框</div>
</div>
<el-button
class="button"
type="primary"
@click="sendMessage"
:icon="Position"
>发送</el-button
>
</div>
</div>
</el-card>
</div>
</template>
<script setup>
import {
ElButton,
ElScrollbar,
ElInput,
ElCard,
ElMessageBox,
ElMessage
} from 'element-plus'
import { Position } from '@element-plus/icons-vue'
import { ref, nextTick } from 'vue'
const messages = ref([])
const input = ref('')
const text = ref('')
const sendMessage = async () => {
if (input.value.trim() !== '') {
messages.value.push({
id: Date.now(),
text: input.value,
timestamp: new Date()
})
input.value = ''
await nextTick() // 等待DOM更新完成
const messageElements = document.getElementsByClassName('message')
const lastMessageElement = messageElements[messageElements.length - 1]
lastMessageElement.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
}
const clearInput = () => {
input.value = ''
}
const formatTimestamp = timestamp => {
const options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
return new Intl.DateTimeFormat('default', options).format(timestamp)
}
const copyMsg = index => {
const text = messages.value[index].text
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
ElMessage({
type: 'success',
message: '复制成功'
})
}
const delateMsg = index => {
ElMessageBox.confirm(`是否删除此数据吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
messages.value.splice(index, 1)
ElMessage({
type: 'success',
message: '删除成功'
})
})
.catch(() => {})
}
</script>
<style lang="less" scoped>
.deploy {
padding: 20px 60px 0 60px;
.cardTop {
margin-bottom: 20px;
color: #27264d;
font-size: 32px;
opacity: 0.85;
display: flex;
}
.elCard {
height: 800px;
.left {
width: 300px;
margin-right: 70px;
flex: 0;
display: flex;
.leftUl {
img {
width: 20px;
height: 20px;
}
.leftLi {
display: flex;
.circle {
position: relative;
}
.circle::after {
content: '';
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, 0);
width: 2px;
height: 740px;
background-color: #75b7ff;
}
}
.leftLi2 {
display: flex;
margin-top: 720px;
}
}
.robot {
display: flex;
flex-direction: column;
justify-content: space-between;
img {
width: 40px;
height: 40px;
}
}
}
.right {
flex: 1;
padding: 0 30px;
border: 2px solid #ccc;
border-radius: 7px;
position: relative;
.chat {
height: 660px;
padding: 10px;
display: flex;
flex-direction: column;
.chat-messages {
flex: 1;
margin-right: 200px;
.message {
display: flex;
flex-direction: column;
margin-top: 50px;
position: relative;
.content {
height: 200px;
background: #f4f6f8;
display: flex;
padding: 2px 0 0 5px;
border: 1px solid #ccc;
word-wrap: break-word;
}
.date {
position: absolute;
align-self: flex-start;
top: -20px;
color: #ccc;
}
.delate {
position: absolute;
align-self: flex-end;
bottom: 0px;
right: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.delate:hover {
cursor: pointer;
}
.delate:hover .text {
opacity: 1;
}
.copy {
position: absolute;
align-self: flex-end;
bottom: 30px;
right: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.copy:hover {
cursor: pointer;
}
.copy:hover .text {
opacity: 1;
}
}
}
}
.chat-input {
display: flex;
align-items: center;
z-index: 1000;
position: absolute;
bottom: 10px;
width: 95%;
.input {
margin-right: 10px;
}
.clear {
width: 20px;
height: 50px;
display: flex;
margin-left: 5px;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
font-size: 12px;
color: #1296db;
margin-left: 5px;
opacity: 0;
}
}
.clear:hover {
cursor: pointer;
width: 100px;
}
.clear:hover .text {
opacity: 1;
margin-right: 15px;
}
.button {
padding: 15px;
}
}
}
}
:deep(.el-card__body) {
display: flex;
width: 100%;
}
:deep(.el-input__inner) {
white-space: pre-wrap;
word-break: break-all;
height: 30px;
}
}
</style>
拓 : 上述聊天机器人不够完美
完美效果如下 : 自己发送数据后 , 系统有返回 , 还可以删除系统或自己问题
完美版的代码如下
<template>
<div class="right">
<el-scrollbar height="400px">
<!-- 所有的回复 -->
<div class="allReply" v-for="(item, index) in messages" :key="item.id">
<!-- 右侧自己的回复 -->
<div class="chat2" v-if="!item.deleted2">
<div class="chat-messages2">
<div class="message">
<div class="content">{{ item.text }}</div>
<div class="date">
{{ formatTimestamp(item.timestamp) }}
</div>
<div class="delate2" @click="delateMsg(index, 'self')">
<div class="text">删除</div>
<img src="../../assets/image/删除.png" alt="" />
</div>
<div class="copy2" @click="copyMsg(index)">
<div class="text">复制</div>
<img src="../../assets/image/复制.png" alt="" />
</div>
</div>
</div>
</div>
<!-- 左侧系统的回复 -->
<div class="chat" v-if="!item.deleted">
<img
src="../../assets/image/gpt.png"
alt=""
srcset=""
class="chatgptLogo"
v-if="isGptShow"
/>
<div class="chat-messages">
<div class="message">
<div class="content">{{ item.textSystem }}</div>
<div class="date">
{{ formatTimestamp(item.timestamp) }}
</div>
<div class="delate" @click="delateMsg(index, 'system')">
<img src="../../assets/image/删除.png" alt="" />
<div class="text">删除</div>
</div>
<div class="copy" @click="copyMsg(index)">
<img src="../../assets/image/复制.png" alt="" />
<div class="text">复制</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div class="chat-input">
<el-input
v-model="input"
@keydown.enter.prevent="sendMessage"
placeholder="试着输入点什么..."
class="input"
type="textarea"
:rows="3"
autosize
:style="{ maxHeight: '90px', overflowY: 'auto' }"
>
</el-input>
<div class="clear" @click="clearInput">
<img src="../../assets/image/清空.png" alt="" />
<div class="text">清空输入框</div>
</div>
<el-button
class="button"
type="primary"
@click="sendMessage"
:icon="Position"
>发送</el-button
>
</div>
</div>
</template>
<script setup>
import {
ElButton,
ElScrollbar,
ElInput,
ElMessageBox,
ElMessage
} from 'element-plus'
import { Position } from '@element-plus/icons-vue'
import { ref, nextTick } from 'vue'
const messages = ref([]) //总回复
const input = ref('')
const text = ref('')
const isGptShow = ref(false)
const sendMessage = async () => {
isGptShow.value = true
if (input.value.trim() !== '') {
const newMessage = {
id: Date.now(),
text: input.value,
textSystem: '我是系统回复...',
timestamp: new Date()
}
messages.value.push(newMessage)
input.value = ''
await nextTick() // 等待DOM更新完成 非常重要
const messageElements = document.getElementsByClassName('message')
const lastMessageElement = messageElements[messageElements.length - 1]
lastMessageElement.scrollIntoView({ behavior: 'smooth', block: 'end' })
}
}
const clearInput = () => {
input.value = ''
}
const formatTimestamp = timestamp => {
const options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
}
return new Intl.DateTimeFormat('default', options).format(timestamp)
}
const copyMsg = index => {
const text = messages1.value[index].text
const textarea = document.createElement('textarea')
textarea.value = text
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
ElMessage({
type: 'success',
message: '复制成功'
})
}
const delateMsg = (index, type) => {
ElMessageBox.confirm(`是否删除此数据吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
// messages.value.splice(index, 1)
if (type === 'system') {
const message = messages.value[index]
delete message.textSystem
message.deleted = true // 标记消息为已删除
} else if (type === 'self') {
const message = messages.value[index]
delete message.text
message.deleted2 = true // 标记消息为已删除
}
ElMessage({
type: 'success',
message: '删除成功'
})
})
.catch(() => {})
}
</script>
<style lang="less" scoped>
.cardTop {
margin-bottom: 20px;
color: #27264d;
font-size: 32px;
opacity: 0.85;
display: flex;
}
.elCard {
height: 800px;
.right {
flex: 1;
padding: 0 30px;
position: relative;
height: 500px;
// 公共区域
.message {
display: flex;
flex-direction: column;
margin-top: 50px;
position: relative;
.content {
height: 200px;
background: #f4f6f8;
display: flex;
padding: 7px 0 0 10px;
border: 1px solid #ccc;
border-radius: 7px;
word-wrap: break-word;
flex-wrap: wrap;
}
.date {
position: absolute;
align-self: flex-start;
bottom: -20px;
right: 0;
color: #ccc;
font-size: 14px;
}
.delate {
position: absolute;
align-self: flex-end;
bottom: 0px;
right: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.delate:hover {
cursor: pointer;
}
.delate:hover .text {
opacity: 1;
}
.copy {
position: absolute;
align-self: flex-end;
bottom: 30px;
right: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.copy:hover {
cursor: pointer;
}
.copy:hover .text {
opacity: 1;
}
}
// 左侧系统的输出
.chat {
padding: 10px;
display: flex;
flex-direction: column;
position: relative;
.chatgptLogo {
width: 20px;
height: 20px;
position: absolute;
top: 30px;
}
.chat-messages {
flex: 1;
margin-right: 200px;
height: 400px;
}
}
// 右侧自己的输出
.chat2 {
padding: 10px;
display: flex;
flex-direction: column;
.chat-messages2 {
flex: 1;
margin-left: 200px;
height: 400px;
.delate2 {
position: absolute;
align-self: flex-end;
bottom: 0px;
left: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.delate2:hover {
cursor: pointer;
}
.delate2:hover .text {
opacity: 1;
}
.copy2 {
position: absolute;
align-self: flex-end;
bottom: 30px;
left: -60px;
display: flex;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
color: #1296db;
opacity: 0;
padding-left: 5px;
}
}
.copy2:hover {
cursor: pointer;
}
.copy2:hover .text {
opacity: 1;
}
}
}
// 输入框区域
.chat-input {
display: flex;
align-items: center;
z-index: 1000;
position: absolute;
bottom: 10px;
width: 95%;
.input {
margin-right: 10px;
// border-top: 1px solid #ccc;
}
.clear {
width: 20px;
height: 50px;
display: flex;
margin-left: 5px;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
.text {
font-size: 12px;
color: #1296db;
margin-left: 5px;
opacity: 0;
}
}
.clear:hover {
cursor: pointer;
width: 100px;
}
.clear:hover .text {
opacity: 1;
margin-right: 15px;
}
.button {
padding: 15px;
}
}
}
:deep(.el-card__body) {
display: flex;
width: 100%;
}
:deep(.el-input__inner) {
white-space: pre-wrap;
word-break: break-all;
height: 27px;
}
}
</style>