<!-- eslint-disable vue/no-deprecated-v-on-native-modifier -->
|
<!-- eslint-disable unicorn/prefer-add-event-listener -->
|
<script lang="ts" setup>
|
import type { UploadChangeParam } from 'ant-design-vue';
|
|
import { computed, nextTick, reactive, ref, toRefs } from 'vue';
|
|
import { useGlobSetting, useMessage } from '@jnpf/hooks';
|
import { checkImgType, createImgPreview } from '@jnpf/ui';
|
|
import { Search } from '@vben/icons';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
|
import dayjs from 'dayjs';
|
|
import { useWebSocket } from '#/hooks/web/useWebSocket';
|
import { $t } from '#/locales';
|
import { getAuthMediaUrl } from '#/utils/jnpf';
|
|
import emojiJson from '../emoji.json';
|
|
interface State {
|
showHistory: boolean;
|
list: any[];
|
otherUser: any;
|
userInfo: any;
|
historyList: any[];
|
emojiList: any[];
|
historyDefaultList: any[];
|
currentPage: number;
|
pageSize: number;
|
finish: boolean;
|
loading: boolean;
|
messageContent: string;
|
keyword: string;
|
popoverVisible: boolean;
|
}
|
|
const { onWebSocket, sendWsMsg } = useWebSocket();
|
|
onWebSocket((data: any) => {
|
// 接收对方发送的消息
|
if (data.method == 'messageList') {
|
initData(data);
|
}
|
});
|
|
const { createMessage } = useMessage();
|
const globSetting = useGlobSetting();
|
const apiUrl = ref(globSetting.apiURL);
|
const uploadUrl = ref(globSetting.uploadURL);
|
const chatContentListRef = ref(null);
|
const historyListRef = ref(null);
|
const userStore = useUserStore();
|
const accessStore = useAccessStore();
|
const state = reactive<State>({
|
showHistory: false,
|
list: [],
|
otherUser: {},
|
userInfo: userStore.getUserInfo,
|
emojiList: emojiJson,
|
historyList: [],
|
historyDefaultList: [],
|
currentPage: 1,
|
pageSize: 50,
|
finish: false,
|
loading: false,
|
messageContent: '',
|
keyword: '',
|
popoverVisible: false,
|
});
|
const { userInfo, list, otherUser, historyList, showHistory, messageContent } = toRefs(state);
|
const getHeaders = computed(() => ({ Authorization: accessStore.accessToken }));
|
|
defineExpose({
|
getMessageList,
|
addItem,
|
clearMsgList,
|
});
|
|
function getMessageList(item) {
|
state.popoverVisible = false;
|
state.showHistory = false;
|
state.otherUser = item;
|
// 获取消息列表
|
const msg = {
|
method: 'MessageList',
|
toUserId: item.id,
|
formUserId: state.userInfo.userId,
|
currentPage: 1,
|
pageSize: 30,
|
sort: 'desc',
|
};
|
sendWsMsg(JSON.stringify(msg));
|
state.list = [];
|
}
|
function clearMsgList() {
|
state.list = [];
|
state.historyList = [];
|
}
|
function initData(data) {
|
const list: any[] = [];
|
for (let i = 0; i < data.list.length; i++) {
|
const item = data.list[i];
|
const message = item.contentType === 'text' ? replaceEmoji(item.content) : JSON.parse(item.content);
|
list.push({
|
userId: item.sendUserId,
|
messageType: item.contentType,
|
message,
|
dateTime: dayjs(item.sendTime).format('YYYY-MM-DD HH:mm:ss'),
|
});
|
}
|
if (state.showHistory) {
|
state.historyList = [...state.historyList, ...list];
|
state.currentPage += 1;
|
state.finish = list.length < data.pagination.pageSize;
|
} else {
|
state.list = list;
|
const num = list.some((o) => o.messageType === 'image') ? 300 : 0;
|
nextTick(() => {
|
scroll(num);
|
});
|
}
|
state.loading = false;
|
}
|
function replaceEmoji(str) {
|
// 替换表情符号为图片
|
const replacedStr = str.replaceAll(/\[([^(\]|[)]*)\]/g, (item) => {
|
let obj = '';
|
for (let i = 0; i < state.emojiList.length; i++) {
|
const row = state.emojiList[i];
|
if (row.alt == item) {
|
obj = `<img src="/resource/emoji/${row.url}" class="chat-content-list-text-emoji" />`;
|
break;
|
}
|
}
|
return obj;
|
});
|
str = replacedStr;
|
return str;
|
}
|
function scroll(num = 0) {
|
setTimeout(() => {
|
nextTick(() => {
|
const ele: any = chatContentListRef.value;
|
if (!ele) return;
|
// 设置滚动条到最底部
|
if (ele.scrollHeight > ele.clientHeight) ele.scrollTo({ top: ele.scrollHeight, behavior: num ? 'smooth' : 'auto' });
|
});
|
}, num);
|
}
|
function addItem(item) {
|
if (item.messageType === 'text') {
|
item.message = replaceEmoji(item.message);
|
}
|
state.list.push(item);
|
scroll(item.messageType === 'image' ? 200 : 0);
|
if (state.showHistory) state.historyList.push(item);
|
}
|
function sendMessage() {
|
if (!state.messageContent) return;
|
const msg = {
|
method: 'SendMessage',
|
toUserId: state.otherUser.id,
|
messageType: 'text',
|
messageContent: state.messageContent,
|
};
|
sendWsMsg(JSON.stringify(msg));
|
state.messageContent = '';
|
}
|
// 选择表情
|
function selectEmoji(item) {
|
state.messageContent += item.alt;
|
state.popoverVisible = false;
|
}
|
function openHistory() {
|
state.showHistory = !state.showHistory;
|
if (!state.showHistory) return;
|
state.historyList = [];
|
state.currentPage = 1;
|
state.pageSize = 50;
|
state.finish = false;
|
sendList();
|
nextTick(() => {
|
bindScroll();
|
});
|
}
|
function sendList() {
|
state.loading = true;
|
const msg = {
|
method: 'MessageList',
|
toUserId: state.otherUser.id,
|
formUserId: state.userInfo.userId,
|
currentPage: state.currentPage,
|
pageSize: state.pageSize,
|
sort: 'asc',
|
keyword: state.keyword,
|
};
|
sendWsMsg(JSON.stringify(msg));
|
}
|
function bindScroll() {
|
const ele: any = historyListRef.value;
|
if (!ele) return;
|
ele.addEventListener('scroll', () => {
|
if (state.finish || state.loading) return;
|
if (ele.scrollTop >= ele.scrollHeight - ele.clientHeight - 100) sendList();
|
});
|
}
|
function searchHistory() {
|
state.currentPage = 1;
|
state.pageSize = 50;
|
state.finish = false;
|
state.historyList = [];
|
sendList();
|
nextTick(() => {
|
const ele: any = historyListRef.value;
|
if (!ele) return;
|
ele.scrollTop = 0;
|
});
|
}
|
function beforeUpload(file) {
|
if (!checkImgType(file)) {
|
createMessage.error('请上传图片');
|
return false;
|
}
|
const isRightSize = file.size < 1024 * 1024 * 5;
|
if (!isRightSize) createMessage.error(`图片大小不能超过5M`);
|
return isRightSize;
|
}
|
function handleChange({ file }: UploadChangeParam) {
|
if (file.status === 'error') {
|
createMessage.error('上传失败');
|
return;
|
}
|
if (file.status === 'done') {
|
if (file.response.code === 200) {
|
if (!file.response.data || !file.response.data.name) return;
|
const name = file.response.data.name;
|
getBase64(file.originFileObj).then((data: any) => {
|
getImgSize(data.e).then((res: any) => {
|
const messageContent: any = {
|
name,
|
width: res.width,
|
height: res.height,
|
};
|
const msg = {
|
method: 'SendMessage',
|
toUserId: state.otherUser.id,
|
messageType: 'image',
|
messageContent,
|
};
|
sendWsMsg(JSON.stringify(msg));
|
nextTick(() => {
|
scroll(300);
|
});
|
});
|
});
|
} else {
|
createMessage.error(file.response.msg);
|
}
|
}
|
}
|
function getImgSize(event) {
|
return new Promise((resolve, reject) => {
|
const size = { width: 0, height: 0 };
|
const txt = event.target.result;
|
const img = document.createElement('img');
|
img.src = txt;
|
img.addEventListener('load', () => {
|
size.width = img.width;
|
size.height = img.height;
|
resolve(size);
|
});
|
img.onerror = function (error) {
|
reject(error);
|
};
|
});
|
}
|
function getBase64(file) {
|
return new Promise((resolve, reject) => {
|
const reader: any = new FileReader();
|
const msg = { base64: '', e: null };
|
reader.readAsDataURL(file);
|
reader.addEventListener('load', (event) => {
|
msg.base64 = reader.result.replace(/data:image\/.*;base64,/, '');
|
msg.e = event;
|
});
|
reader.onerror = function (error) {
|
reject(error);
|
};
|
reader.onloadend = function () {
|
resolve(msg);
|
};
|
});
|
}
|
function handlePreview(url) {
|
createImgPreview({ imageList: [getAuthMediaUrl(url)], zIndex: 10000 });
|
}
|
</script>
|
|
<template>
|
<div class="im-container">
|
<div class="chatBox">
|
<div class="chat-content-header">
|
<a-avatar :size="32" :src="apiUrl + otherUser.headIcon" v-if="otherUser?.headIcon" />
|
<span class="name" v-if="otherUser?.realName">{{ otherUser.realName }}/{{ otherUser.account }}</span>
|
</div>
|
<div class="chat-content-main">
|
<div class="chat-content-list" ref="chatContentListRef">
|
<div
|
class="chat-content-list-item"
|
v-for="(item, index) in list"
|
:key="index"
|
:class="{ 'chat-content-list-item--mine': item.userId == userInfo.userId }">
|
<div class="chat-content-list-text">
|
<div class="chat-content-list-time">
|
<cite>{{ item.dateTime }}</cite>
|
</div>
|
<p v-if="item.messageType == 'text'" v-html="item.message" class="chat-content-list__msg--text"></p>
|
<img
|
:src="getAuthMediaUrl(item.message.path)"
|
class="chat-content-list__msg--img"
|
@click="handlePreview(item.message.path)"
|
v-if="item.messageType == 'image' && item.message.path" />
|
<audio
|
class="chat-content-list__msg--audio"
|
controls
|
:src="getAuthMediaUrl(item.message.path)"
|
v-if="item.messageType == 'voice' && item.message.path"></audio>
|
</div>
|
</div>
|
<div class="chat-content-empty" v-if="!list.length">
|
<jnpf-empty />
|
</div>
|
</div>
|
<div class="writeBox">
|
<div class="toolBox">
|
<div class="toolBox-left">
|
<a-popover placement="topLeft" trigger="click" overlay-class-name="emoji-popover" v-model:open="state.popoverVisible">
|
<i class="link-text ym-custom ym-custom-emoticon-neutral" title="发送表情"></i>
|
<template #content>
|
<div class="emojiBox">
|
<ul class="emoji">
|
<li v-for="(item, i) in state.emojiList" :key="i" @click="selectEmoji(item)">
|
<img :src="`/resource/emoji/${item.url}`" />
|
</li>
|
</ul>
|
</div>
|
</template>
|
</a-popover>
|
<a-upload
|
:show-upload-list="false"
|
:action="`${uploadUrl}/IM`"
|
class="uploadImg-btn"
|
:headers="getHeaders"
|
accept="image/*"
|
:before-upload="beforeUpload"
|
@change="handleChange">
|
<i class="link-text ym-custom ym-custom-image" title="发送图片"></i>
|
</a-upload>
|
</div>
|
<div class="toolBox-right" @click="openHistory">
|
<i class="link-text icon-ym icon-ym-generator-time"></i>
|
<span>聊天记录</span>
|
</div>
|
</div>
|
<div class="writeBox-main">
|
<jnpf-textarea placeholder="点击这里,直接输入消息咨询" v-model:value.trim="messageContent" @keyup.enter="sendMessage()" />
|
<a-button type="primary" @click="sendMessage()" class="send-btn"><i class="icon-ym icon-ym-release"></i></a-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="historyBox" v-if="showHistory">
|
<div class="historyBox-header">
|
<a-input :placeholder="$t('common.drawerSearchText')" allow-clear v-model:value="state.keyword" :bordered="false" @keyup.enter="searchHistory">
|
<template #suffix>
|
<Search class="size-4" @click="searchHistory" />
|
</template>
|
</a-input>
|
</div>
|
<div class="historyList-box" ref="historyListRef">
|
<div class="chat-content-list historyList">
|
<div class="chat-content-list-item chat-content-list-item--history" v-for="(item, index) in historyList" :key="index">
|
<div class="chat-content-list-text">
|
<div class="chat-content-list-time" v-if="item.userId == userInfo.userId">
|
<cite>
|
<span class="mr-[6px]">我</span>
|
<i>{{ item.dateTime }}</i>
|
</cite>
|
</div>
|
<div class="chat-content-list-time" v-else>
|
<cite>
|
<span class="mr-[6px]">{{ otherUser.realName }}</span>
|
<i>{{ item.dateTime }}</i>
|
</cite>
|
</div>
|
<span v-if="item.messageType == 'text'" v-html="item.message"></span>
|
<img
|
:src="getAuthMediaUrl(item.message.path)"
|
class="chat-content-list__msg--img"
|
@click="handlePreview(item.message.path)"
|
v-if="item.messageType == 'image' && item.message.path" />
|
<audio
|
class="chat-content-list__msg--audio"
|
controls
|
:src="getAuthMediaUrl(item.message.path)"
|
v-if="item.messageType == 'voice' && item.message.path"></audio>
|
</div>
|
</div>
|
</div>
|
<jnpf-empty v-if="!historyList.length" />
|
</div>
|
</div>
|
</div>
|
</template>
|