由于项目需要需要使用vue3 setup + js
实现简易版web端聊天组件,特此记录一下,相关代码如下:
WebChat.vue 组件:
<template>
<div class="chat-container">
<div class="chat-area pr"
ref="chatAreaRef">
<div class="loading-box"
v-if="isLoading">正在加载中...</div>
<div class="loading-box"
v-if="!isLoadingNextPage">暂无更多数据~~</div>
<div v-for="(item,index) in msgList"
:key="index"
:class="['message',item.createBy !== userInfo.userId ? 'message-left' : 'message-right']"
:id="`msg-${item.groupChatMessageId}`"
:ref="(el) => {refMsgNodesList[item.groupChatMessageId] = el} ">
<el-avatar class="avatar-iocn"
:size="40"
:src="img_man_avater"
fit="cover" />
<div class="item flex-col-b"
:style="{
'--align-self':item.createBy !== userInfo.userId?'flex-start':'flex-end',
'--bg-color':item.createBy !== userInfo.userId?'#f5f5f5':'#00b0c16e',
}">
<div class="name">
{{item?.userInfo?.departmentName }}{{$dictValue('accountType', item?.userInfo?.userType)}}
{{item?.userInfo?.realName}}
{{formatDate(item?.createTime, "yyyy-MM-dd hh:mm:ss")}}
</div>
<!-- 文本 -->
<div v-if="item.messageType == 1"
class="text">
<div class="text-box">{{item.messageContent}}</div>
</div>
<!-- 图片 -->
<div v-else-if="item.messageType == 2"
class="text">
<el-image class="img-box"
draggable="false"
:src="`${fileUrl}${item.messageContent}`"
:zoom-rate="1.2"
:preview-src-list="[`${fileUrl}${item.messageContent}`]"
:hide-on-click-modal="true"
fit="cover" />
</div>
<!-- 语音 -->
<div v-else-if="item.messageType == 3"
class="text">
<audio :src="`${fileUrl}${item.messageContent}`"
style="height: 50px;width: 300px"
controls>
</audio>
</div>
<!-- 视频 -->
<div v-else-if="item.messageType == 4"
class="text">
<video controls
preload="auto"
width="280"
height="180"
style="object-fit: fill">
<source :src="`${fileUrl}${item.messageContent}`"
type="video/mp4" />
</video>
</div>
</div>
</div>
</div>
<div class="input-area pr">
<div class="tools flex-sc">
<div class="tools-item">
<g-upload-img v-model="imgUrl"
:slotStant="true"
:showFileList="false"
:isEcho="false"
:onSuccess="onSuccess"
listType="text"
:type="['jpg', 'png', 'bmp', 'jpeg', 'gif']"
url="upload-image"
size="5120k"
:wh="[1920, 1500]">
<template #icon>
<el-icon :size="24"
color="#5f6f85">
<PictureFilled />
</el-icon>
</template>
</g-upload-img>
</div>
<div class="tools-item">
<g-upload-video id="uploadVideo"
v-model="videoUrl"
:type="['mp4']"
:on-success="handleVideoSuccess"
size="10m"
:isTip="false"
:iconSize="24"
:isDefaultIcon="false"
tip="只能上传mp4格式文件,大小不超过10m">
<template #icon>
<el-icon :size="24"
color="#5f6f85">
<VideoCameraFilled />
</el-icon>
</template>
</g-upload-video>
</div>
</div>
<div class="input">
<el-input ref="inputRef"
v-model="inputValue"
maxlength="500"
:autofocus="true"
style="width: 100%"
rows='4'
@keyup.enter="enterSend"
type="textarea" />
<!-- <EditorInput ref="editorInputRef"
v-model="inputValue"
@focusFn="focusFn"></EditorInput> -->
</div>
</div>
<div class="btn flex-e pr">
<el-button :disabled="!inputValue"
@click="send">发送(Enter)</el-button>
<!-- <div class="empty-tip"></div> -->
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, getCurrentInstance, computed, onUnmounted, watch } from 'vue'
import { useStore } from 'vuex';
const props = defineProps({})
const { proxy } = getCurrentInstance();
const store = useStore();
const userInfo = xxx //当前登录用户信息
const inputValue = ref('') // 输入框内容
const imgUrl = ref('') // 图片地址
const videoUrl = ref('') // 视频地址
const scrollTop = ref(0) // 滚动条距离
const refMsgNodesList = ref({}); // 消息节点列表,用户向上滚动加载滚动定位
const pageIndex = ref(1)
const pageSize = ref(defaultsetting.defaultPageSize)
const pageTotal = ref(0)
const isLoading = ref(false) // 是否正在加载数据
const chatAreaDom = ref(null) // 聊天区域dom
// 消息列表
const msgList = ref([])
// 回车发送
const enterSend = (event) => {
if (event.keyCode === 13 && !event.shiftKey && !event.ctrlKey) {
send()
}
}
// 发送消息
const sendMessage = (type, text) => {
let message = {
"query": {}
}
// 长连接
(webChat/sendMessage', {message: message }) ..................
}
// 设置本人发送的消息
const setSelfMessage = (type, text) => {
let obj = {}
msgList.value.push(obj)
if (type === 1) {
inputValue.value = ''
}
scrollToBottom()
}
// 发送消息
const send = () => {
if (!inputValue.value.trim()) {
proxy.$message.warning('不能发送空白消息')
return
}
sendMessage(1, inputValue.value)
// setSelfMessage(1, inputValue.value)
}
// 上传图片成功回调
const onSuccess = (res) => {
}
// 上传视频成功回调
const handleVideoSuccess = (res) => {
}
const isLoadingNextPage = ref(false)
// 获取聊天信息
const getChatMessage = async (isFristLoading = false) => {
try {
let topId = msgList.value?.[0]?.groupChatMessageId // 消息id topId
const res = await API({
"pageIndex": 1,
"pageSize": 20,
"query": {}
})
if (res.code === 2000) {
// console.log('获取聊天信息', res);
if (res?.result.length === 0) {
isLoadingNextPage.value = false
return
} else {
isLoadingNextPage.value = true
}
msgList.value = [...res?.result, ...msgList.value]
pageTotal.value = res?.totalRows || 0
nextTick(() => {
if (isFristLoading) {
scrollToBottom()
} else {
refMsgNodesList.value[topId]?.scrollIntoView()
}
})
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
isLoading.value = false;
}
}
// 监听滚动条位置
const scrollFn = () => {
chatAreaDom.value = proxy.$refs.chatAreaRef;
chatAreaDom.value.addEventListener('scroll', handleScroll);
}
// 滚动条事件
const handleScroll = () => {
scrollTop.value = chatAreaDom.value.scrollTop;
// console.log('滚动条位置', scrollTop.value);
// 判断是否滚动到顶部且不在加载状态
if (scrollTop.value === 0 && !isLoading.value) {
isLoading.value = true;
console.log('加载数据');
if (isLoadingNextPage.value) {
pageIndex.value++
setTimeout(() => {
getChatMessage();
}, 300)
} else {
isLoading.value = false;
}
}
}
// 获取焦点
const onFocusFn = () => {
nextTick(() => {
proxy.$refs.inputRef.focus()
})
}
// 滚动条置底
const scrollToBottom = (isInstant = true) => {
nextTick(() => {
chatAreaDom.value?.scrollTo({
top: chatAreaDom.value.scrollHeight,
behavior: isInstant ? 'instant' : 'smooth',
});
})
}
//监听推送过来的某个卡片数据信息
watch(() => chatMsg, (val) => {
if (!val) return
msgList.value = [...msgList.value, val]
inputValue.value = ''
setTimeout(() => {
scrollToBottom()
}, 50)
})
// 建立连接
const initWebChatMsg = () => {}
onMounted(() => { })
onUnmounted(() => {
webChat/closeSignalR............. //关闭连接
chatAreaDom.value?.removeEventListener('scroll', handleScroll);
})
defineExpose({
onFocusFn,
scrollFn,
scrollToBottom,
getChatMessage,
initWebChatMsg,
})
</script>
<style lang="scss" scoped>
.chat-container {
width: 100%;
height: 650px;
overflow: hidden;
display: flex;
flex-direction: column;
.chat-area {
flex: 1;
overflow-y: auto;
border-bottom: 1px solid #00000020;
border-top: 1px solid #00000020;
.loading-box {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
background: transparent;
}
.message {
display: flex;
width: 100%;
padding: 10px;
border-radius: 10px;
margin-bottom: 15px;
word-wrap: break-word;
}
.message-left {
flex-direction: row;
align-content: flex-start;
// background-color: #f5f5f5;
}
.message-right {
flex-direction: row-reverse;
align-content: flex-start;
// background-color: #00b0c1;
}
.avatar-iocn {
margin: 0 10px;
}
.item {
.name {
margin-bottom: 5px;
display: flex;
align-self: var(--align-self);
}
.text {
max-width: 450px;
min-height: 40px;
display: flex;
justify-content: var(--align-self);
.text-box {
padding: 10px;
border-radius: 8px;
width: fit-content;
background-color: var(--bg-color);
color: #131414;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
word-break: break-all;
}
.img-box {
border-radius: 8px;
width: 200px;
border-radius: 8px;
height: 100%;
}
}
}
}
.input-area {
height: 150px;
position: relative;
.tools {
position: absolute;
left: 0;
top: 5px;
height: 30px;
line-height: 30px;
:deep(.logo-upload .el-upload) {
border: none;
margin-top: 2px;
}
.tools-item {
width: 30px;
height: 30px;
cursor: pointer;
margin-right: 2px;
}
}
.input {
margin-top: 35px;
height: 100px;
overflow: auto;
:deep(.el-textarea__inner) {
outline: none;
border: none;
resize: none;
box-shadow: none;
background: transparent;
padding: 0;
}
}
}
.btn {
display: flex;
flex-direction: row-reverse;
align-content: flex-start;
button {
text-align: center;
height: 32px;
padding: 5px 16px;
flex-shrink: 0;
border-radius: 4px;
background: #f1f1f1;
outline: none;
border: none;
color: #429f4f;
font-size: 16px;
font-weight: 400;
}
}
}
</style>