1.先去GoEasy官网下载源码
第一步 App.vue
<script setup lang="ts">
import { watch, ref, markRaw, reactive, nextTick, provide, InjectionKey } from 'vue'
import headerIndex from '@/Layout/header/headerIndex.vue'
import purchaseHeaderBig from '@/Layout/header/purchaseHeader/purchaseHeaderBig/index.vue'
import purchaseHeaderSmall from '@/Layout/header/purchaseHeader/purchaseHeaderSmall/index.vue'
import { Local } from '@/utils/storage'
import { ElConfigProvider, ElMessage, ElScrollbar } from 'element-plus'
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
import { useRoute, useRouter } from 'vue-router'
import { useTokenStore, goEasyStore } from '@/store/index'
import GoEasyUi from '@/components/goEasy/GoEasyUi.vue'
import SvgIcon from '@/components/SvgIcon/index.vue'
import GoEasyHome from '@/components/goEasy/Home.vue'
import { loginInfo, dictionaryList, mouldTypeList } from '@/api/login/index'
import { getNodeTree } from '@/api/demand/index'
import { app as VueApp } from './main'
const userStore = useTokenStore()
const route = useRoute()
const router = useRouter()
const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
let srcollTop = 0
let beforeSrcollTop = 0
// 个人认证成功重新请求 获取userInfo
watch(
() => route.fullPath,
(newPath) => {
// if ((newPath.includes('status=2') && userStore.status != 2) || userStore.isAdmin) {
// }
if (newPath.includes('status=2') && userStore.status != 2) {
loginInfo().then((res) => {
userStore.heardImgUrl = res.data.heardImgUrl
Local.set('userInfo', res.data)
userStore.nickname = res.data.username
userStore.status = res.data.status
userStore.appType = res.data.appType
userStore.isSupplier = res.data.isSupplier
if (userStore.appType == '1') {
userStore.isAdmin = true
} else {
userStore.isAdmin = res.data.isAdmin
}
})
}
},
{ immediate: true }
)
watch(
() => route.path,
(newPath) => {
if (newPath === '/') {
setTimeout(() => {
scrollbarRef.value?.scrollTo({ left: 0, top: beforeSrcollTop, behavior: 'smooth' })
}, 20)
}
newPath && !_isMobile() ? scrollbarRef.value?.setScrollTop(0) : ''
}
)
// // 捕获异常刷新页面
// window.addEventListener(
// 'error',
// (error) => {
// console.log('捕获异常', error)
// let target: any = error.target || error.srcElement
// if (target instanceof HTMLScriptElement || target instanceof HTMLLinkElement) {
// router.go(0)
// }
// },
// true
// )
// if (useTokenStore().token) {
// LayIM.init(getCurrentInstance(), 500)
// }
const _isMobile = () => {
let flag = navigator.userAgent.match(
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
)
return flag
}
if (_isMobile()) {
router.push({
name: 'temporaryMobileTerminal',
})
}
dictionaryList().then((res) => {
// Local.set('dictionary', res.data)
let data = {} as any
mouldTypeList().then((result) => {
res.data.push({
code: 'MOULD_TYPE',
item: result.data,
})
res.data.forEach((item) => {
data[item.code] = item
})
Local.set('dictionary', data)
})
})
const goEasyService = (): void => {
if (useTokenStore().token) {
useTokenStore().goEasyVisible = true
} else {
ElMessage.warning('未登录')
router.push('/login')
}
}
const GoEasyHomeRef = ref<InstanceType<typeof GoEasyHome>>()
useTokenStore().initGoEasy = useTokenStore().token ? true : false
watch(
() => useTokenStore().initGoEasy,
(newPath) => {
if (!newPath) {
GoEasyHomeRef.value?.disconnect()
} else {
GoEasyHomeRef.value?.initGoEasy()
}
}
)
if (useTokenStore().initGoEasy) {
GoEasyHomeRef.value?.initGoEasy()
}
watch(
() => useTokenStore().backToTopFLag,
(newValue) => {
if (newValue) {
useTokenStore().backToTopFLag = false
scrollbarRef.value?.scrollTo({
left: 0,
top: 0,
behavior: 'smooth',
})
}
}
)
const scroll = (data) => {
if (route.path === '/') {
srcollTop = data.scrollTop
}
// if (route.fullPath == '/purchase/purchaseLogin' || route.fullPath == '/purchase/login/purchaseRegister') {
// return true
// }
// if (route.fullPath.indexOf('purchase') !== -1) {
// if (data.scrollTop >= 100) {
// // 集采平台
// currentHeaderComponent.comp = headerComponent[2].com
// headerHeight.value = 'height:100px'
// } else {
// // 集采平台
// currentHeaderComponent.comp = headerComponent[1].com
// headerHeight.value = 'height:194px'
// }
// }
}
router.beforeEach((to, from) => {
if (from.path === '/') {
beforeSrcollTop = srcollTop
}
})
const headerComponent = reactive<any[]>([
{
name: '设计制造平台头部',
com: markRaw(headerIndex),
},
{
name: '集采平台大头部',
com: markRaw(purchaseHeaderBig),
},
{
name: '集采平台小头部',
com: markRaw(purchaseHeaderSmall),
},
])
let currentHeaderComponent = reactive<any>({
comp: headerComponent[0].com,
})
const headerHeight = ref('')
const isShowPurchaseHeader = ref(false)
watch(
() => route,
(newRoute) => {
if (newRoute.fullPath == '/purchase/purchaseLogin' || newRoute.fullPath == '/purchase/login/purchaseRegister') {
currentHeaderComponent.comp = headerComponent[0].com
headerHeight.value = 'height:64px'
isShowPurchaseHeader.value = false
} else if (newRoute.fullPath.indexOf('purchase') !== -1) {
// 集采平台
// currentHeaderComponent.comp = headerComponent[1].com
headerHeight.value = 'height:194px;display:none'
isShowPurchaseHeader.value = true
} else {
// 设计制造平台
currentHeaderComponent.comp = headerComponent[0].com
headerHeight.value = 'height:64px'
isShowPurchaseHeader.value = false
}
},
{ immediate: true, deep: true }
)
// 全局获取nodeTree
const map = ref<any>({})
const mapChildren = ref<any>({})
const getAllNodeTree = async () => {
VueApp.config.globalProperties.$treeNodeMap = (id: any) => {
return map.value[id]
}
VueApp.config.globalProperties.$treeNodeMapCode = (id: any) => {
return mapChildren.value[id]
}
const { data } = await getNodeTree()
Local.set('qt_nodeTree', JSON.stringify(data))
// 设置全局id和name的字典库
loopData(data)
}
const loopData = (arr) => {
for (let i = 0; i < arr.length; i++) {
const current = arr[i]
map.value[current.nodeId] = current.name
if (current.children.length > 0)
mapChildren.value[current.nodeId] = current.children.map((item) => item.nodeCode).filter((item) => item != null)
if (current.children) {
loopData(current.children)
}
}
}
getAllNodeTree()
const isRouterAlive = ref(true)
const reload = () => {
isRouterAlive.value = false
nextTick(() => {
isRouterAlive.value = true
})
}
provide(`reload`, reload)
// console.log(mapChildren.value['1664531691763863553'], 'mapChildren.valuemapChildren.valuemapChildren.value')
</script>
<template>
<div ref="main-test" class="common-layout">
<el-container v-if="!_isMobile()">
<el-header :style="headerHeight"> <component :is="currentHeaderComponent.comp"></component></el-header>
<el-scrollbar ref="scrollbarRef" class="appMain" @scroll="scroll">
<el-config-provider :locale="zhCn">
<el-main style="min-width: 1200px"
><div class="mainDev">
<purchaseHeaderBig v-if="isShowPurchaseHeader" />
<router-view v-if="isRouterAlive" v-slot="{ Component }">
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.name" />
<keep-alive>
<component :is="Component" v-if="route.meta.keepAlive" :key="route.name" />
</keep-alive>
</router-view>
<GoEasyUi v-if="useTokenStore().initGoEasy"></GoEasyUi>
<!-- <GoEasyHome v-if="useTokenStore().initGoEasy" ref="GoEasyHomeRef" style="display: none"></GoEasyHome> -->
<div v-if="useTokenStore().initGoEasy" class="service" @click="goEasyService">
<SvgIcon icon-class="_comments" size="24" />
<span v-if="goEasyStore().unreadAmount" class="menu-unread">{{ goEasyStore().unreadAmount }}</span>
</div>
</div>
</el-main>
</el-config-provider>
</el-scrollbar>
</el-container>
<el-container v-else>
<el-config-provider :locale="zhCn">
<el-main
><div class="mainDev">
<router-view />
</div>
</el-main>
</el-config-provider>
</el-container>
</div>
</template>
<style scoped lang="scss">
:deep(.el-scrollbar__wrap) {
background: #f5f4f4;
}
.el-overlay {
z-index: 9999999999998 !important;
}
#app {
width: 100%;
font-size: 14px;
}
.common-layout {
overflow: hidden !important;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.text {
color: aqua;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
.el-scrollbar__view {
height: auto;
}
.service {
position: fixed;
display: flex;
right: 0;
width: 42px;
height: 42px;
bottom: 183px;
border-radius: 50%;
background: radial-gradient(50% 50% at 50% 50%, #ffffff 0%, #fafafa 58%, #f6f6f6 100%);
box-sizing: border-box;
border: 1px solid #ffffff;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
align-items: center;
justify-content: center;
margin-right: 16px;
cursor: pointer;
z-index: 99;
.svg-icon {
width: 24px !important;
height: 24px;
}
}
.menu-unread {
position: absolute;
top: -5px;
right: 5px;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background-color: #d02129;
color: #ffffff;
}
</style>
第二步 聊天弹框 GoEasyUi.vue
<script setup lang="ts" name="">
import { ref, watch } from 'vue'
import GoEasyHome from '@/components/goEasy/Home.vue'
import { useTokenStore, goEasyStore } from '@/store/index'
const dialogVisible = ref(false)
watch(
() => useTokenStore().goEasyVisible,
(newValue) => {
if (newValue) {
dialogVisible.value = newValue
}
},
{ immediate: true }
)
const GoEasyHomeRef = ref<InstanceType<typeof GoEasyHome>>()
const handleClose = (): void => {
dialogVisible.value = false
useTokenStore().messageHistory = false
useTokenStore().goEasyVisible = false
// goEasyStore().privateChatVisible = false
}
</script>
<template>
<el-dialog
v-model="dialogVisible"
:close-on-click-modal="false"
:modal="false"
destroy-on-close
width="850px"
:loading="!goEasyStore().privateChatVisible"
:class="useTokenStore().messageHistory ? 'goEasy goEasys' : 'goEasy'"
@close="handleClose">
<div class="chat">
<el-container>
<el-main>
<GoEasyHome ref="GoEasyHomeRef"></GoEasyHome>
</el-main>
</el-container>
</div>
</el-dialog>
</template>
第三步 home.vue
<script setup lang="ts">
import { currentUserType, currentUserInfoType } from './type/goEasyType'
import { Local } from '@/utils/storage'
import { ref, getCurrentInstance, markRaw } from 'vue'
import { goEasyOtpApi } from './api/goEasyApi'
import Conversations from './Conversations.vue'
import Contacts from './Contacts.vue'
import { goEasyStore, useTokenStore } from '@/store/index'
const currentUser = ref({} as currentUserType)
const goEasy: any = getCurrentInstance()?.proxy?.goEasy
const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
const goEasyOtpData = ref<string>('')
const initGoEasy = (): void => {
currentUser.value.id = goEasyData.value?.tenantCode
currentUser.value.avatar = goEasyData.value?.avatar
currentUser.value.phone = goEasyData.value?.mobile
currentUser.value.email = goEasyData.value?.email
currentUser.value.name = goEasyData.value?.tenantName
goEasyOtpApi().then((result) => {
goEasyOtpData.value = result.data
console.log(result.data, 'goEasyOtpData.value')
console.log(goEasy.getConnectionStatus(), useTokenStore().initGoEasy, 478392749238)
if (goEasy.getConnectionStatus() === 'disconnected' && useTokenStore().initGoEasy) {
connectGoEasy()
}
menuChange(itemNames.value)
goEasy.im?.on(GoEasy?.IM_EVENT?.CONVERSATIONS_UPDATED, setUnreadNumber)
})
}
const disconnect = (): void => {
goEasy.disconnect({
onSuccess: function () {
console.log('GoEasy disconnect successfully.')
},
onFailed: function (error) {
console.log('Failed to disconnect GoEasy, code:' + error.code + ',error:' + error.content)
},
})
}
const currentComponentList = ref([
{
name: '当前会话',
com: markRaw(Conversations),
},
{
name: '好友列表',
com: markRaw(Contacts),
},
])
const currentComponent = ref()
initGoEasy()
defineExpose({
initGoEasy,
disconnect,
})
const connectGoEasy = () => {
goEasy.connect({
otp: goEasyOtpData.value,
id: currentUser.value.id,
data: { name: currentUser.value.name, avatar: currentUser.value.avatar },
onSuccess: function () {
//连接成功
console.log('GoEasy connect successfully.') //连接成功
},
onFailed: function (error: { code: string; content: string }) {
//连接失败
console.log('Failed to connect GoEasy, code:' + error.code + ',error:' + error.content)
},
onProgress: function (attempts: any) {
//连接或自动重连中
console.log('GoEasy is connecting', attempts)
},
})
}
const setUnreadNumber = (content: { conversations: any[]; unreadTotal: string }) => {
goEasyStore().unreadAmount = 0
console.log(content.conversations, 'content.conversations')
content.conversations.map((item) => {
if (item.userId === goEasyStore().userId) {
item.unread = 0
}
goEasyStore().unreadAmount += item.unread
})
}
const itemNames = ref('当前会话')
const menuChange = (itemName: string) => {
itemNames.value = itemName
currentComponent.value = currentComponentList.value.find((v) => v.name == itemName)?.com
}
</script>
<template>
<div class="home">
<div class="home-container">
<div class="home-menu">
<div class="menu-header">
<img class="user-avatar" :src="currentUser.avatar" />
<div class="user-profile">
<div class="user-profile-main">
<div class="user-profile-header">
<img :src="currentUser.avatar" />
<div>{{ currentUser.name }}</div>
</div>
<div class="user-profile-info">
<div class="user-profile-info-title">邮箱</div>
<div>{{ currentUser.email }}</div>
</div>
<div class="user-profile-info">
<div class="user-profile-info-title">手机</div>
<div>{{ currentUser.phone }}</div>
</div>
</div>
</div>
</div>
<div class="menu-box">
<div class="menu-list">
<div :class="itemNames == '当前会话' ? 'menu-item i_t' : 'menu-item'" @click="menuChange('当前会话')">
<i class="iconfont icon-zaixiankefu"></i>
<span v-if="goEasyStore().unreadAmount" class="menu-unread">{{ goEasyStore().unreadAmount }}</span>
</div>
<!-- <div :class="itemNames == '好友列表' ? 'menu-item i_t' : 'menu-item'" @click="menuChange('好友列表')">
<i class="iconfont icon-haoyou"></i>
</div> -->
</div>
</div>
</div>
<div class="home-main">
<component :is="currentComponent" v-if="useTokenStore().goEasyVisible"></component>
<!-- <router-view /> -->
</div>
</div>
</div>
</template>
<style scoped>
@media screen and (max-height: 720px) {
.home-container {
min-width: 850px;
height: 500px;
}
}
@media screen and (min-height: 720px) {
.home-container {
min-width: 850px;
height: 600px;
}
}
.home {
width: 100%;
height: 100%;
display: flex;
}
.home-container {
/* width: 959px; */
background: #ffffff;
display: flex;
position: relative;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.home-menu {
width: 60px;
background-color: #f7f7f7;
border-right: 1px solid #eeeeee;
display: flex;
flex-direction: column;
align-items: center;
}
.menu-header {
margin: 20px auto;
}
.user-avatar {
width: 28px;
height: 28px;
border-radius: 10px;
cursor: pointer;
}
.user-avatar:hover + .user-profile {
display: block;
}
.user-profile {
display: none;
color: #ffffff;
position: absolute;
top: 0;
left: 59px;
width: 250px;
height: 200px;
background: #ffffff;
z-index: 999;
}
.user-profile-main {
border: 1px solid #ebeef5;
background-color: #fff;
color: #303133;
border-radius: 4px;
}
.user-profile-header {
padding: 18px 20px;
border-bottom: 1px solid #ebeef5;
display: flex;
flex-direction: column;
align-items: center;
font-size: 15px;
font-weight: bold;
}
.user-profile-header img {
width: 45px;
height: 45px;
}
.user-profile-info {
display: flex;
padding: 10px 20px;
font-size: 14px;
color: #666666;
line-height: 28px;
}
.user-profile-info-title {
width: 70px;
}
.menu-box {
padding: 40px 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.menu-list {
display: flex;
flex-direction: column;
align-items: center;
}
.menu-item {
color: #303133;
cursor: pointer;
height: 56px;
position: relative;
}
.menu-unread {
position: absolute;
top: -5px;
right: 5px;
width: 18px;
height: 18px;
line-height: 18px;
text-align: center;
border-radius: 50%;
background-color: #d02129;
color: #ffffff;
}
.i_t > i {
color: #d02129 !important;
}
.iconfont {
padding: 15px;
font-size: 18px;
color: #606266;
cursor: pointer;
}
.iconfont:active {
color: #d02129;
}
.home-main {
padding: 0;
flex: 1;
}
</style>
第四步 当前会话组件 Conversations.vue
<template>
<div class="conversations">
<div class="conversation-list">
<div class="conversation-list-container">
<div class="conversation-list-content">
<div v-if="conversations.length">
<div v-for="(conversation, key) in conversations" :key="key" replace @click="chatLocation(conversation)">
<div
class="conversation"
:style="{
background:
conversation.id == goEasyStore().userId || conversation.userId == goEasyStore().userId
? '#EBEEF5'
: '',
}"
@contextmenu.prevent.stop="(e) => showRightClickMenu(e, conversation)">
<div class="avatar">
<el-image style="width: 40px; height: 40px" :src="conversation.data.avatar" fit="scale-down" />
<!-- <img :src="conversation.data.avatar" width="40" height="40" /> -->
<div v-if="conversation.unread > 0" class="unread-count">
<span class="unread">{{ conversation.unread }}</span>
</div>
</div>
<div class="conversation-message">
<div class="conversation-top">
<span class="conversation-name">{{ conversation.data.name }}</span>
<div class="conversation-time">
<div>{{ formatDate(conversation.lastMessage?.timestamp) }}</div>
</div>
</div>
<div v-if="conversation.lastMessage" class="conversation-bottom">
<div v-if="conversation.lastMessage.recalled" class="conversation-content">
<div v-if="conversation.type === 'private'">
{{
conversation.lastMessage.senderId === currentUser.id
? '你'
: `"${conversation.data.name}"`
}}撤回了一条消息
</div>
<div v-if="conversation.type === 'group'">
{{
conversation.lastMessage.senderId === currentUser.id
? '你'
: `"${conversation.lastMessage.senderData.name}"`
}}撤回了一条消息
</div>
</div>
<div v-else class="conversation-content">
<div
v-if="
conversation.lastMessage.read === false &&
conversation.lastMessage.senderId === currentUser.id
"
class="unread-text">
[未读]
</div>
<div v-if="conversation.type === 'private'">
{{ conversation.lastMessage.senderId === currentUser.id ? '我' : conversation.data.name }}:
</div>
<div v-else>
{{
conversation.lastMessage.senderId === currentUser.id
? '我'
: conversation.lastMessage.senderData.name
}}:
</div>
<span v-if="conversation.lastMessage.type === 'text'" class="text">{{
conversation.lastMessage.payload.text
}}</span>
<span v-else-if="conversation.lastMessage.type === 'video'">[视频消息]</span>
<span v-else-if="conversation.lastMessage.type === 'audio'">[语音消息]</span>
<span v-else-if="conversation.lastMessage.type === 'qt_image'">[图片消息]</span>
<span v-else-if="conversation.lastMessage.type === 'qt_file'">[文件消息]</span>
<span v-else-if="conversation.lastMessage.type === 'order'">[订单消息]</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="no-conversation">- 当前没有会话 -</div>
</div>
</div>
<div v-if="rightClickMenu.visible" :style="{ left: rightClickMenu.x + 'px', top: rightClickMenu.y + 'px' }" class="action-box">
<div class="action-item" @click="topConversation">{{ rightClickMenu?.conversation?.top ? '取消置顶' : '置顶' }}</div>
<div class="action-item" @click="deleteConversation">删除聊天</div>
</div>
</div>
<div class="chat">
<PrivateChat v-if="goEasyStore().privateChatVisible" :key="goEasyStore().userId"></PrivateChat>
<!-- <router-view :key="$route.params.id" /> -->
</div>
</div>
</template>
<script setup lang="ts">
import { formatDate } from '@/utils/GoEasyData'
import PrivateChat from './PrivateChat.vue'
import { conversationsType, currentUserInfoType, currentUserType, friendType } from './type/goEasyType'
import { Local } from '@/utils/storage'
import { useTokenStore, goEasyStore } from '@/store/index'
import { goEasyServiceId } from './api/goEasyApi'
import { ref, getCurrentInstance, onBeforeUnmount } from 'vue'
// const privateChatVisible = ref(goEasyStore().privateChatVisible)
// console.log('🚀 ~ file: Conversations.vue:95 ~ privateChatVisible:', privateChatVisible.value)
const currentUser = ref({} as currentUserType)
const conversations = ref<Array<conversationsType>>([])
const conversationPorp = ref({} as conversationsType)
const goEasy: any = getCurrentInstance()?.proxy?.goEasy
const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
const rightClickMenu = ref<any>({
conversation: '',
visible: false,
x: null,
y: null,
})
currentUser.value.id = goEasyData.value.tenantCode
currentUser.value.avatar = goEasyData.value.avatar
currentUser.value.phone = goEasyData.value.mobile
currentUser.value.name = goEasyData.value.tenantName
const serviceIdData = ref({} as friendType)
document.querySelector('.home')?.addEventListener('click', () => {
hideRightClickMenu()
})
onBeforeUnmount(() => {
goEasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, renderConversations)
})
formatDate
const loadConversations = async () => {
console.log(serviceIdData.value, 'fsdklfjsdklfjksdl')
await goEasyServiceId()
.then((result) => {
serviceIdData.value = result.data
console.log('111')
console.log('🚀 ~ file: Conversations.vue:138 ~ goEasyServiceId111111111111 ~ result:', result)
})
.catch((err) => {
console.log('🚀 ~ file: Conversations.vue:141 ~ goEasyServiceId ~ err:', err)
})
goEasy.im.latestConversations({
onSuccess: (result: { content: any }) => {
console.log('🚀 ~ file: Conversations.vue:133 ~ loadConversations ~ result:', result)
let content = result.content
renderConversations(content)
},
onFailed: (error) => {
console.log('获取最新会话列表失败, code:' + error.code + 'content:' + error.content)
},
})
}
const listenConversationUpdate = () => {
// 监听会话列表变化
goEasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, renderConversations)
}
const renderConversations = (content: { conversations: any }) => {
console.log('2222')
let serviceData = false
content.conversations.map((item: { type: string; unread: number; top: boolean }) => {
if (item.type === 'cs') {
console.log('true')
serviceData = true
}
})
console.log(content.conversations)
console.log(serviceData, 'serviceData')
if (!serviceData) {
content.conversations.unshift({
data: {
name: serviceIdData.value.name,
avatar: serviceIdData.value.avatar,
},
top: true,
type: 'cs',
unread: 0,
id: serviceIdData.value.id,
})
}
conversations.value = content.conversations
}
// const subscribeGroup = () => {
// let groups = restApi.findGroups(currentUser.value)
// let groupIds = groups.map((item) => item.id)
// goEasy.im.subscribeGroup({
// groupIds: groupIds,
// onSuccess: function () {
// console.log('订阅群消息成功')
// },
// onFailed: function (error) {
// console.log('订阅群消息失败:', error)
// },
// })
// }
const showRightClickMenu = (e: any, conversation: any) => {
if (conversation.type == 'cs') return
rightClickMenu.value.conversation = conversation
rightClickMenu.value.visible = true
rightClickMenu.value.x = e.pageX
rightClickMenu.value.y = e.pageY
}
const hideRightClickMenu = () => {
rightClickMenu.value.visible = false
}
const topConversation = () => {
let conversation: any = rightClickMenu.value.conversation
let description = conversation.top ? '取消置顶' : '置顶'
goEasy.im.topConversation({
conversation: conversation,
top: !conversation.top,
onSuccess: function () {
console.log(description, '成功')
},
onFailed: function (error) {
console.log(description, '失败:', error)
},
})
}
const deleteConversation = () => {
if (confirm('确认要删除这条会话吗?')) {
let conversation = rightClickMenu.value.conversation
goEasy.im.removeConversation({
conversation: conversation,
onSuccess: function () {
console.log('删除会话成功')
},
onFailed: function (error: any) {
console.log(error)
},
})
}
}
const chatLocation = (conversation: conversationsType) => {
conversationPorp.value = conversation
console.log('🚀 ~ file: Conversations.vue:211 ~ chatLocation ~ conversation:', conversation)
goEasyStore().$patch({
userId: conversation.type == 'cs' ? conversation.id : conversation.userId,
name: conversation.data.name,
avatar: conversation.data.avatar,
privateChatVisible: true,
goEasyType: conversation.type,
receiverId: conversation.type == 'cs' ? conversation.id : conversation.userId,
})
console.log(goEasyStore().receiverId, 'goEasyStore()goEasyStore()goEasyStore()')
useTokenStore().messageHistory = false
}
listenConversationUpdate() //监听会话列表变化
loadConversations() //加载会话列表
// subscribeGroup() //订阅群消息
</script>
<style scoped>
.conversations {
width: 100%;
height: 100%;
position: relative;
display: flex;
color: #333333;
}
.conversation-list {
width: 220px;
}
.conversation-list-container {
height: 100%;
display: flex;
flex-direction: column;
background-color: white;
border-right: #dbd6d6 1px solid;
}
.conversation-list-content {
flex: 1;
overflow-y: auto;
padding: 10px 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.conversation-list-content::-webkit-scrollbar {
display: none;
}
.no-conversation {
text-align: center;
color: #666666;
}
.conversation {
display: flex;
padding: 10px 5px;
cursor: pointer;
}
.unread-count {
position: absolute;
top: -10px;
left: 30px;
width: 18px;
height: 18px;
border-radius: 50%;
color: white;
background: #d02129;
}
.unread-count .unread {
display: block;
font-size: 12px;
text-align: center;
line-height: 18px;
}
.conversation-message {
flex: 1;
padding-left: 5px;
display: flex;
width: 160px;
flex-direction: column;
justify-content: space-around;
font-size: 12px;
}
.conversation-top {
display: flex;
align-items: center;
justify-content: space-between;
text-align: right;
}
.conversation-name {
width: 89px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
font-weight: 500;
text-align: left;
}
.conversation-time {
width: 80px;
color: #b9b9b9;
font-size: 12px;
display: flex;
flex-direction: column;
}
.conversation-bottom {
display: flex;
color: #666666;
}
.conversation-content {
display: flex;
width: 160px;
color: #b3b3b3;
}
.conversation-content .text {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
white-space: nowrap;
word-break: break-all;
}
.conversation-bottom .unread-text {
color: #d02129;
/* width: 35px !important; */
}
.conversation .avatar {
width: 40px;
height: 40px;
position: relative;
}
.conversation .avatar img {
width: 100%;
border-radius: 10%;
}
.router-link-active {
background: #eeeeee;
}
.action-box {
width: 100px;
height: 60px;
background: #ffffff;
border: 1px solid #cccccc;
position: fixed;
z-index: 100;
border-radius: 5px;
}
.action-box .action-item {
padding-left: 15px;
line-height: 30px;
font-size: 13px;
color: #262628;
cursor: pointer;
}
.action-box .action-item:hover {
background: #dddddd;
}
.chat {
min-width: 570px;
display: flex;
}
</style>
第五步 私聊组件 PrivateChat.vue
<!-- eslint-disable @typescript-eslint/no-non-null-assertion -->
<!-- eslint-disable vue/no-setup-props-destructure -->
<template>
<div class="chat-container">
<div class="chat-title">
<!-- <img :src="friend.avatar" class="chat-avatar" /> -->
<div class="chat-name">{{ friend.name }}</div>
</div>
<div ref="scrollView" class="chat-main">
<div ref="messageList" class="message-list">
<div v-if="history.loading" class="history-loading">
<img src="@/assets/images/pending.gif" />
</div>
<div v-else :class="history.allLoaded ? 'history-loaded' : 'load'" @click="loadHistoryMessage(false)">
{{ history.allLoaded ? '已经没有更多的历史消息' : '获取历史消息' }}
</div>
<div v-for="(message, index) in history.messages" :key="index">
<div class="time-tips">{{ renderMessageDate(message, index) }}</div>
<div v-if="message.recalled" class="message-recalled">
<div v-if="message.senderId !== currentUser.id">{{ friend.name }}撤回了一条消息</div>
<div v-else class="message-recalled-self">
<div>你撤回了一条消息</div>
<span
v-if="message.type === 'text' && Date.now() - message.timestamp < 60 * 1000"
@click="editRecalledMessage(message.payload.text)"
>重新编辑</span
>
</div>
</div>
<div v-else class="message-item">
<div v-if="messageSelector.visible && message.status !== 'sending'" class="message-item-checkbox">
<input
v-model="messageSelector.ids"
class="input-checkbox"
type="checkbox"
:value="message.messageId"
@click="selectMessages" />
</div>
<div v-if="message.type === 'CS_ACCEPT'" class="accept-message">{{ message.senderData.name }}已接入</div>
<div v-else-if="message.type === 'CS_END'" class="accept-message">{{ message.senderData.name }}已结束会话</div>
<div v-else-if="message.type === 'CS_PRE_AUTO_END'" class="accept-message">
请问还在线吗?如没有问题,系统将在{{ message.payload.text }}秒后自动结束会话
</div>
<div v-else-if="message.type === 'CS_TRANSFER'" class="accept-message">
{{ message.senderData.name + `已转接给` + message.payload.transferTo.data.name }}
</div>
<div class="message-item-content" :class="{ self: message.senderId === currentUser.id }">
<div v-if="message.senderData" class="sender-info">
<img v-if="currentUser.id === message.senderId" :src="message.senderData.avatar" class="sender-avatar" />
<img v-else :src="message.senderData.avatar" class="sender-avatar" />
</div>
<div v-else class="sender-info">
<img v-if="currentUser.id === message.senderId" :src="currentUser.avatar" class="sender-avatar" />
<img v-else :src="friend.avatar" class="sender-avatar" />
</div>
<div class="message-content" @click.right="showActionPopup(message)">
<div class="message-payload">
<div v-if="message.status === 'sending'" class="pending"></div>
<div v-if="message.status === 'fail'" class="send-fail"></div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div
v-if="message.type === 'text'"
class="content-text"
v-html="emoji.decoder.decode(message.payload.text)"></div>
<div
v-if="message.type === 'qt_image'"
class="content-image"
@click="showImagePreviewPopup(message.payload.url)">
<img
:src="message.payload.url"
:style="{ height: getImageHeight(message.payload.width, message.payload.height) + 'px' }" />
<!-- -->
</div>
<a v-if="message.type === 'qt_file'" :href="message.payload.url" target="_blank" download="download">
<div class="content-file" title="点击下载">
<div class="file-info">
<span class="file-name">{{ message.payload.name }}</span>
<span class="file-size">{{ (message.payload.size / 1024).toFixed(2) }}KB</span>
</div>
<img class="file-img" src="../../assets/images/file.png" />
</div>
</a>
<div v-if="message.type === 'audio'" class="content-audio" @click="playAudio(message)">
<div class="audio-facade" :style="{ width: Math.ceil(message.payload.duration) * 7 + 50 + 'px' }">
<div
class="audio-facade-bg"
:class="{ 'play-icon': audioPlayer.playingMessage === message }"></div>
<div>{{ Math.ceil(message.payload.duration) || 1 }}<span>"</span></div>
</div>
</div>
<!-- <goeasy-video-player
v-if="message.type === 'videos'"
:thumbnail="message.payload.thumbnail"
:src="message.payload.video.url" /> -->
<div v-if="message.type === 'order'" class="content-order">
<div class="order-id">订单号:{{ message.payload.id }}</div>
<div class="order-body">
<img :src="message.payload.url" class="order-img" />
<div class="order-name">{{ message.payload.name }}</div>
<div>
<div class="order-price">{{ message.payload.price }}</div>
<div class="order-count">共{{ message.payload.count }}件</div>
</div>
</div>
</div>
</div>
<div v-if="currentUser.id === message.senderId" :class="message.read ? 'message-read' : 'message-unread'">
<div v-if="message.senderId === currentUser.id && goEasyStore().goEasyType != 'cs'">
{{ message.read ? '已读' : '未读' }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="chat-footer">
<div v-if="messageSelector.visible" class="action-delete">
<img class="delete-btn" src="../../assets/images/delete.png" @click="deleteMultipleMessages" />
<div>删除</div>
</div>
<div v-else class="action-box">
<div class="action-bar">
<!-- 表情 -->
<div class="action-item">
<div v-if="emoji.visible" class="emoji-box">
<img
v-for="(emojiItem, emojiKey, index) in emoji.map"
:key="index"
class="emoji-item"
:src="emoji.url + emojiItem"
@click="chooseEmoji(emojiKey)" />
</div>
<i class="iconfont icon-smile" title="表情" @click="emoji.visible = !emoji.visible"></i>
</div>
<!-- 图片 -->
<div class="action-item">
<label for="img-input">
<i class="iconfont icon-picture" title="图片"></i>
</label>
<!-- <up-load-avatar ref="upLoadAvatarRef" title="" :font-size="14" @upload-url="getLogoUrl"></up-load-avatar> -->
<input v-show="false" id="img-input" accept="image/*" multiple type="file" @change="sendImageMessage" />
</div>
<!-- 视频 -->
<!-- <div class="action-item">
<label for="video-input"><i class="iconfont icon-film" title="视频"></i></label>
<input v-show="false" id="video-input" accept="video/*" type="file" @change="sendVideoMessage" />
</div> -->
<!-- 文件 -->
<div class="action-item">
<label for="file-input">
<i class="iconfont icon-wj-wjj" title="文件"></i>
</label>
<input v-show="false" id="file-input" type="file" @change="sendFileMessage" />
</div>
<!-- 自定义-订单消息 -->
<!-- <div class="action-item">
<i class="iconfont icon-liebiao" title="订单" @click="showOrderMessageList"></i>
</div> -->
<div
class="action-item last-action-item"
:style="{ color: useTokenStore().messageHistory ? '#3760F1' : '' }"
@click="messageHistory">
<SvgIcon v-if="useTokenStore().messageHistory" icon-class="active_history" />
<SvgIcon v-else icon-class="_history" />
聊天记录
<!-- <i class="iconfont icon-liebiao" title="订单" @click="showOrderMessageList"></i> -->
</div>
</div>
<!-- GoEasyIM最大支持3k的文本消息,如需发送长文本,需调整输入框maxlength值 -->
<div class="input-box">
<textarea
ref="input"
v-model="text"
maxlength="700"
autocomplete="off"
class="input-content"
@focus="onInputFocus"
@keyup.enter="sendTextMessage"></textarea>
</div>
<div class="send-box">
<el-button type="primary" @click="sendTextMessage">发送</el-button>
</div>
</div>
</div>
<!-- 语音播放器 -->
<audio ref="audioPlayerRef" @ended="onAudioPlayEnd" @pause="onAudioPlayEnd"></audio>
<!-- 图片预览弹窗 -->
<el-image-viewer v-if="imagePreview.visible" :url-list="[imagePreview.url]" @close="hideImagePreviewPopup" />
<!-- <div v-if="imagePreview.visible" class="image-preview">
<img :src="imagePreview.url" alt="图片" />
<span class="close" @click="hideImagePreviewPopup">×</span>
</div> -->
<!-- 消息删除撤回弹窗 -->
<!-- <div v-if="actionPopup.visible" class="action-popup" @click="actionPopup.visible = false">
<div class="action-popup-main">
<div class="action-item" @click="deleteSingleMessage">删除</div>
<div v-if="actionPopup.recallable" class="action-item" @click="recallMessage">撤回</div>
<div class="action-item" @click="showCheckBox">多选</div>
<div class="action-item" @click="actionPopup.visible = false">取消</div>
</div>
</div> -->
<!-- 订单弹窗 -->
<!-- <div v-if="orderList.visible" class="order-box">
<div class="order-list">
<div class="title">
<div>请选择一个订单</div>
<span @click="closeOrderMessageList">×</span>
</div>
<div v-for="(order, index) in orderList.orders" :key="index" class="order-item" @click="sendOrderMessage(order)">
<div class="order-id">订单号:{{ order.id }}</div>
<div class="order-body">
<img :src="order.url" class="order-img" />
<div class="order-name">{{ order.name }}</div>
<div>
<div class="order-price">{{ order.price }}</div>
<div class="order-count">共{{ order.count }}件</div>
</div>
</div>
</div>
</div>
</div> -->
</div>
<div v-if="useTokenStore().messageHistory" class="home-record">
<div class="home-record-title"></div>
<div class="home-record-tabList">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="全部" name="all">
<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
<ul v-infinite-scroll="load" class="messageHistory-list">
<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
<div style="display: flex">
<div class="messageHistory-list-item-avatar">
<img :src="item.avatar" alt="" srcset="" />
</div>
<div class="messageHistory-list-item-right">
<div class="messageHistory-list-item-name">
{{ item.username }}
<span>{{ formatDate(item.timestamp) }}</span>
</div>
<div
v-if="item.type == '1'"
class="messageHistory-list-item-content-text"
v-html="emoji.decoder.decode(JSON.parse(item.content).text)"></div>
<div
v-if="item.type == '2'"
class="messageHistory-list-item-content-qt_image"
@click="showImagePreviewPopup(JSON.parse(item.content).url)">
<div class="content-image">
<img
:src="JSON.parse(item.content).url"
:style="{
height:
getImageHeight(
JSON.parse(item.content).width,
JSON.parse(item.content).height
) + 'px',
}" />
<!-- -->
</div>
</div>
<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
<div class="content-file" title="点击下载">
<div class="file-info">
<span class="file-name">{{ JSON.parse(item.content).name }}</span>
<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
</div>
<img class="file-img" src="../../assets/images/file.png" />
</div>
</a>
</div>
</div>
</li>
</ul>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="图片" name="image">
<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
<ul v-infinite-scroll="load" class="messageHistory-list">
<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
<div style="display: flex">
<div class="messageHistory-list-item-avatar">
<img :src="item.avatar" alt="" srcset="" />
</div>
<div class="messageHistory-list-item-right">
<div class="messageHistory-list-item-name">
{{ item.username }}
<span>{{ formatDate(item.timestamp) }}</span>
</div>
<div v-if="item.type == '1'" class="messageHistory-list-item-content-text">
{{ JSON.parse(item.content).text }}
</div>
<div
v-if="item.type == '2'"
class="messageHistory-list-item-content-qt_image"
@click="showImagePreviewPopup(JSON.parse(item.content).url)">
<div class="content-image">
<img
:src="JSON.parse(item.content).url"
:style="{
height:
getImageHeight(
JSON.parse(item.content).width,
JSON.parse(item.content).height
) + 'px',
}" />
<!-- -->
</div>
</div>
<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
<div class="content-file" title="点击下载">
<div class="file-info">
<span class="file-name">{{ JSON.parse(item.content).name }}</span>
<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
</div>
<img class="file-img" src="../../assets/images/file.png" />
</div>
</a>
</div>
</div>
</li>
</ul>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane label="文件" name="file">
<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%">
<ul v-infinite-scroll="load" class="messageHistory-list">
<li v-for="(item, index) in messageHistoryData" :key="index" class="messageHistory-list-item">
<div style="display: flex">
<div class="messageHistory-list-item-avatar">
<img :src="item.avatar" alt="" srcset="" />
</div>
<div class="messageHistory-list-item-right">
<div class="messageHistory-list-item-name">
{{ item.username }}
<span>{{ formatDate(item.timestamp) }}</span>
</div>
<div v-if="item.type == '1'" class="messageHistory-list-item-content-text">
{{ JSON.parse(item.content).text }}
</div>
<div
v-if="item.type == '2'"
class="messageHistory-list-item-content-qt_image"
@click="showImagePreviewPopup(JSON.parse(item.content).url)">
<div class="content-image">
<img
:src="JSON.parse(item.content).url"
:style="{
height:
getImageHeight(
JSON.parse(item.content).width,
JSON.parse(item.content).height
) + 'px',
}" />
<!-- -->
</div>
</div>
<a v-if="item.type == '3'" :href="JSON.parse(item.content).url" target="_blank" download="download">
<div class="content-file" title="点击下载">
<div class="file-info">
<span class="file-name">{{ JSON.parse(item.content).name }}</span>
<span class="file-size">{{ (JSON.parse(item.content).size / 1024).toFixed(2) }}KB</span>
</div>
<img class="file-img" src="../../assets/images/file.png" />
</div>
</a>
</div>
</div>
</li>
</ul>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { formatDate } from '@/utils/GoEasyData'
import { ElScrollbar } from 'element-plus'
import { goEasyHistoryMessageApi } from './api/goEasyApi'
import EmojiDecoder from './utils/EmojiDecoder'
import SvgIcon from '@/components/SvgIcon/index.vue'
import { useTokenStore, goEasyStore } from '@/store/index'
import axios from 'axios'
import { getImPublicSignedUrl, SignReq } from '@/api/upDate/file'
import type { TabsPaneContext } from 'element-plus'
import {
currentUserInfoType,
currentUserType,
historyType,
friendType,
toType,
historyMessagesType,
queryType,
messageHistory,
} from './type/goEasyType'
import { Local } from '@/utils/storage'
import { ref, getCurrentInstance, onBeforeUnmount, nextTick } from 'vue'
const IMAGE_MAX_WIDTH = 200
const IMAGE_MAX_HEIGHT = 150
// 图片拼接地址
const emojiUrl = '/goEasyEmoji/'
const emojiMap = {
'[微笑]': 'smiling-face-with-smiling-eyes.png',
'[笑哭]': 'face-with-tears-of-joy.png',
'[龇牙]': 'miling-eyes.png',
'[惊讶]': 'flushed-face.png',
'[得意]': 'smiling-face-with-sunglasses.png',
'[呵呵]': 'smirking-face.png',
'[大笑]': 'happy-face.png',
'[天使]': 'smiling-face-with-halo.png',
'[害怕]': 'open-mouth.png',
'[惊吓]': 'face-screaming-in-fear.png',
'[大哭]': 'loudly-crying-face.png',
'[眨眼]': 'winking-face.png',
'[难过]': 'disappointed-face-2.png',
'[悲伤]': 'tired-face.png',
'[亲亲]': 'kissing-face-with-closed-eyes.png',
'[心累]': 'sleepy-face.png',
'[愤怒]': 'angry-face.png',
'[头晕]': 'face-with-spiral-eyes.png',
'[无奈]': 'crying-face.png',
'[开心]': 'smiling-face-with-heart-eyes.png',
'[哼]': 'confused-face.png',
'[没眼看]': 'dizzy-face.png',
'[庆祝]': 'Partying-Face.png',
'[生气]': 'face-with-steam-from-nose.png',
'[高兴]': 'grinning-face-with-smiling-eyes.png',
'[汗颜]': 'downcast-face-with-sweat.png',
'[惶恐]': 'anxious-face-with-sweat.png',
'[憋住]': 'expressionless-face.png',
'[气愤]': 'pouting-face.png',
'[无语]': 'neutral-face.png',
'[难受]': 'confounded-face.png',
'[不开心]': 'disappointed-face.png',
}
const activeName = ref('all')
const audioPlayerRef = ref<InstanceType<typeof Audio>>()
const scrollView = ref<InstanceType<typeof HTMLDivElement>>()
const messageList = ref<InstanceType<typeof HTMLDivElement>>()
const currentUser = ref({} as currentUserType)
const goEasyData = ref(Local.get('userInfo') as currentUserInfoType)
currentUser.value.id = goEasyData.value.tenantCode
currentUser.value.avatar = goEasyData.value.avatar
currentUser.value.phone = goEasyData.value.mobile
currentUser.value.name = goEasyData.value.tenantName
const pageNum = ref(1)
const query = ref<queryType>({
size: 15,
userId: goEasyStore().receiverId,
type: '',
})
const totalPage = ref(0)
const messageHistoryData = ref<Array<messageHistory>>([])
const goEasy: any = getCurrentInstance()?.proxy?.goEasy
const GoEasy: any = getCurrentInstance()?.proxy?.GoEasy
const friend = ref({
id: goEasyStore().userId,
name: goEasyStore().name,
avatar: goEasyStore().avatar,
} as friendType)
//用于创建消息时传入
const to = ref({
type: goEasyStore().goEasyType == 'cs' ? GoEasy.IM_SCENE.CS : GoEasy.IM_SCENE.PRIVATE,
id: goEasyStore().userId,
data: {
name: goEasyStore().name,
avatar: goEasyStore().avatar,
},
} as toType)
const history = ref({
messages: [],
allLoaded: false,
loading: true,
} as historyType)
const text = ref('')
//定义表情列表
const emoji = ref({
url: emojiUrl,
map: emojiMap,
visible: false,
decoder: new EmojiDecoder(emojiUrl, emojiMap),
})
// 订单消息
// const orderList = ref({
// orders: [],
// visible: false,
// } as orderListType)
// 图片预览弹出框
const imagePreview = ref({
visible: false,
url: '',
})
const audioPlayer = ref({
playingMessage: null,
})
// 展示消息删除弹出框
const actionPopup = ref({
visible: false,
message: null,
recallable: false,
})
const signReq = ref({
method: 'put',
} as SignReq)
const messageSelector = ref({ visible: false, ids: [] as Array<string> })
/**
* 获取消息以便于标记已读
* @param message 父组件传递的消息主体
*/
const onReceivedPrivateMessage = (message: historyMessagesType): void => {
let messages = ref(message)
console.log('🚀 ~ file: PrivateChat.vue:360 ~ onReceivedPrivateMessage ~ message:', message)
if (messages.value.senderId === goEasyStore().userId || messages.value.teamId === goEasyStore().userId) {
history.value.messages.push(messages.value)
markPrivateMessageAsRead()
}
scrollToBottom()
}
const goEasyType = ref(
goEasyStore().goEasyType == 'cs' ? GoEasy.IM_EVENT.CS_MESSAGE_RECEIVED : GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED
)
goEasy.im.on(goEasyType.value, onReceivedPrivateMessage)
onBeforeUnmount(() => {
goEasy.im.off(goEasyType.value, onReceivedPrivateMessage)
})
formatDate
/**
* 核心就是设置高度,产生明确占位
*
* 小 (宽度和高度都小于预设尺寸)
* 设高=原始高度
* 宽 (宽度>高度)
* 高度= 根据宽度等比缩放
* 窄 (宽度<高度)或方(宽度=高度)
* 设高=MAX height
*
* @param width,height
* @returns number
*/
const getImageHeight = (width: number, height: number): number | undefined => {
if (width < IMAGE_MAX_WIDTH && height < IMAGE_MAX_HEIGHT) {
return height
} else if (width > height) {
return (IMAGE_MAX_WIDTH / width) * height
} else if (width === height || width < height) {
return IMAGE_MAX_HEIGHT
}
}
/**
* 用于调起语音
* @param audioMessage 语音组件的消息主体
*/
const playAudio = (audioMessage: any): void => {
let playingMessage = audioPlayer.value.playingMessage
if (playingMessage) {
audioPlayerRef.value?.pause()
// 如果点击的消息正在播放,就认为是停止播放操作
if (playingMessage === audioMessage) {
return
}
}
if (audioPlayerRef.value) {
audioPlayer.value.playingMessage = audioMessage
;(audioPlayerRef.value as InstanceType<typeof Audio>).src = audioMessage.payload.url
audioPlayerRef.value?.load()
;(audioPlayerRef.value as InstanceType<typeof Audio>).currentTime = 0
audioPlayerRef.value?.play()
}
}
/**
* 调起播放语音
*/
const onAudioPlayEnd = (): void => {
audioPlayer.value.playingMessage = null
}
/**
* 发送按钮函数,发送消息
*/
const sendTextMessage = (): void => {
if (!text.value.trim()) {
console.log('输入为空')
return
}
goEasy.im.createTextMessage({
text: text.value,
to: to.value,
onSuccess: (message: historyMessagesType) => {
console.log(message, 'message')
sendMessage(message)
text.value = ''
},
onFailed: (err) => {
console.log('创建消息err:', err)
},
})
}
/**
* 消息输入匡
*/
const onInputFocus = (): void => {
emoji.value.visible = false
}
/**
* 表情函数,用于拼接表情的url
* @param emojiKey 标签的key
*/
const chooseEmoji = (emojiKey: string): void => {
text.value += emojiKey
emoji.value.visible = false
}
/**
* 发送图片的函数
* @param image 图片
*/
const sendImageMessage = (image: Event): void => {
let fileList: any = [...(<HTMLInputElement>image.target).files]
const imageObj = new Image()
fileList &&
fileList.forEach(async (file) => {
let imageData = {
fileName: '',
url: '',
size: '',
width: 0,
height: 0,
contentType: file.type,
}
signReq.value.fileName = file.name
signReq.value.size = file.size
imageData.size = file.size
imageData.fileName = file.name
await getImPublicSignedUrl(signReq.value).then(async (res) => {
imageData.url = res.data.downloadUrl
await upImFile(res.data.actualSignedRequestHeaders, res.data.signedUrl, file)
imageObj.src = res.data.downloadUrl
})
imageObj.addEventListener('load', () => {
imageData.width = imageObj.width
imageData.height = imageObj.height
const imageMessage = goEasy.im.createCustomMessage({
type: 'qt_image',
payload: imageData,
to: to.value,
})
sendMessage(imageMessage)
})
})
}
/**
* 上传图片
* @param header 请求头
* @param url 请求地址
* @param data 包含参数
*/
const upImFile = async (header: any, url: string, data: any) => {
const config = {
method: 'put',
url: url,
headers: header,
data: data,
}
await axios(config)
.then(function (response) {
console.log(JSON.stringify(response.data), 999999999999)
})
.catch(function (error) {
console.log(error)
})
}
/**
* 发送文件函数
* @param files 文件
*/
const sendFileMessage = async (files: Event): Promise<void> => {
const file: any = (<HTMLInputElement>files.target).files?.[0]
const filesData = {
name: '',
url: '',
size: 0,
contentType: file.type,
}
signReq.value.fileName = file.name
signReq.value.size = file.size
filesData.size = file.size
filesData.name = file.name
await getImPublicSignedUrl(signReq.value).then(async (res) => {
filesData.url = res.data.downloadUrl
// 上传文件或图片
await upImFile(res.data.actualSignedRequestHeaders, res.data.signedUrl, file)
})
const filesMessage = goEasy.im.createCustomMessage({
type: 'qt_file',
payload: filesData,
to: to.value,
})
sendMessage(filesMessage)
}
/**
* 发送消息函数
* @param message 消息主体详情
*/
const sendMessage = (message: historyMessagesType): void => {
let messages = ref(message)
history.value.messages.push(messages.value)
console.log(history.value.messages, 'history.value.messages')
scrollToBottom()
goEasy.im.sendMessage({
message: messages.value,
onSuccess: (message: any) => {
console.log('发送成功', message)
},
onFailed: function (error: { code: number }) {
console.log('发送失败', error)
},
})
}
/**
* 聊天记录函数
*/
const messageHistory = (): void => {
useTokenStore().messageHistory = !useTokenStore().messageHistory
if (useTokenStore().messageHistory) {
historyMessageFn()
}
}
/**
* 获取聊天记录函数
*/
const historyMessageFn = (): void => {
goEasyHistoryMessageApi(pageNum.value, query.value).then((result) => {
result.data.list.map((item) => messageHistoryData.value.push(item))
totalPage.value = result.data.totalPage
})
}
/**
* el-tab-pane点击事件
* @param tab el-tab-pane的name
* @param event el-tab-pane的元素
*/
const handleClick = (tab: TabsPaneContext) => {
if (tab.props.name == 'all') {
query.value.type = ''
pageNum.value = 1
messageHistoryData.value = []
historyMessageFn()
} else if (tab.props.name == 'image') {
query.value.type = '2'
pageNum.value = 1
messageHistoryData.value = []
historyMessageFn()
} else {
query.value.type = '3'
pageNum.value = 1
messageHistoryData.value = []
historyMessageFn()
}
}
/**
* 滚动加载函数
*/
const load = () => {
pageNum.value += 1
if (pageNum.value <= totalPage.value) {
historyMessageFn()
}
}
/**
* 弹出框
* @param message 消息主体
*/
const showActionPopup = (message: historyMessagesType): void => {
const MAX_RECALLABLE_TIME = 3 * 60 * 1000 //3分钟以内的消息才可以撤回
messageSelector.value.ids.push(message.messageId)
if (
Date.now() - message.timestamp < MAX_RECALLABLE_TIME &&
message.senderId === currentUser.value.id &&
message.status === 'success'
) {
actionPopup.value.recallable = true
} else {
actionPopup.value.recallable = false
}
actionPopup.value.visible = true
}
/**
* 删除多个
*/
const deleteMultipleMessages = (): void => {
if (messageSelector.value.ids.length > 0) {
messageSelector.value.visible = false
deleteMessage()
}
}
/**
* 删除消息函数
*/
const deleteMessage = (): void => {
let conf = confirm('确认删除?')
if (conf === true) {
let selectedMessages = [] as Array<historyMessagesType>
selectedMessages = history.value.messages.filter((message) => {
return messageSelector.value.ids.includes(message.messageId)
})
// history.value.messages.forEach((message) => {
// if (messageSelector.value.ids.includes(message.messageId)) {
// selectedMessages.push(message)
// }
// })
goEasy.im.deleteMessage({
messages: selectedMessages,
onSuccess: () => {
selectedMessages.forEach((message) => {
let index = history.value.messages.indexOf(message)
if (index > -1) {
history.value.messages.splice(index, 1)
}
})
messageSelector.value.ids = []
},
onFailed: (error) => {
console.log('error:', error)
},
})
} else {
messageSelector.value.ids = []
}
}
/**
* 重新编辑函数
* @param texts 重新编辑的消息
*/
const editRecalledMessage = (texts: string): void => {
text.value = texts
}
/**
* 图片弹出框
* @param url 地址
*/
const showImagePreviewPopup = (url: string): void => {
imagePreview.value.visible = true
imagePreview.value.url = url
}
/**
* 图片预览弹框
*/
const hideImagePreviewPopup = (): void => {
imagePreview.value.visible = false
}
/**
* 消息多选函数
* @param e 传递进来消息的多选
*/
const selectMessages = (e: Event): void => {
let event = <HTMLInputElement>e.target
if (event.checked) {
messageSelector.value.ids.push(event.value)
} else {
let index = messageSelector.value.ids.indexOf(event.value)
if (index > -1) {
messageSelector.value.ids.splice(index, 1)
}
}
}
/**
* 更多消息点击函数
* @param scrollToBottoms 更多消息参数
*/
const loadHistoryMessage = (scrollToBottoms: boolean): void => {
history.value.loading = true
//历史消息
let lastMessageTimeStamp = null as null | number
let lastMessage = history.value.messages[0]
if (lastMessage) {
lastMessageTimeStamp = lastMessage.timestamp
}
goEasy.im.history({
id: friend.value.id,
lastTimestamp: lastMessageTimeStamp,
limit: 10,
type: to.value.type,
onSuccess: (result: { content: any }) => {
history.value.loading = false
let messages = result.content
if (messages.length === 0) {
history.value.allLoaded = true
} else {
if (lastMessageTimeStamp) {
history.value.messages = messages.concat(history.value.messages)
} else {
history.value.messages = messages
}
if (messages.length < 10) {
history.value.allLoaded = true
}
if (scrollToBottoms) {
scrollToBottom()
console.log(2222222)
//收到的消息设置为已读
markPrivateMessageAsRead()
}
}
},
onFailed: (error: { code: string; content: string }) => {
//获取失败
history.value.loading = false
console.log('获取历史消息失败, code:' + error.code + ',错误信息:' + error.content)
},
})
}
loadHistoryMessage(true)
/**
* 消息已读函数
*/
const markPrivateMessageAsRead = (): void => {
goEasy.im.markMessageAsRead({
id: to.value.id,
type: to.value.type,
onSuccess: function () {
console.log(history.value.messages)
console.log('标记私聊已读成功')
},
onFailed: function (error: any) {
console.log('标记私聊已读失败', error)
},
})
}
/**
* 消息轮动函数
*/
const scrollToBottom = (): void => {
nextTick(() => {
;(scrollView.value as HTMLDivElement).scrollTop = (messageList.value as HTMLDivElement).scrollHeight
})
}
/**
* 每一条消息上的时间
* @param message 消息
* @param index 下标
*/
const renderMessageDate = (message: { timestamp: number }, index: number): string => {
if (index === 0) {
return formatDate(message.timestamp)
} else {
if (message.timestamp - history.value.messages[index - 1].timestamp > 5 * 60 * 1000) {
return formatDate(message.timestamp)
}
}
return ''
}
</script>
<style scoped lang="scss">
.chat-container {
min-width: 570px;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.chat-title {
height: 48px;
padding: 0;
display: flex;
align-items: center;
font-size: 18px;
border-bottom: 1px solid #dcdfe6;
}
.chat-avatar {
width: 35px;
height: 35px;
}
.chat-name {
width: 400px;
margin-left: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
font-family: AlibabaPuHuiTiM;
font-size: 16px;
font-weight: 700;
line-height: 24px;
color: #3d3d3d;
letter-spacing: 0px;
}
.chat-main {
display: flex;
flex-direction: column;
overflow-y: auto;
flex: 1;
scrollbar-width: thin;
border-right: 1px solid #dcdfe6;
}
.chat-main::-webkit-scrollbar {
width: 0;
}
.chat-main .history-loaded {
text-align: center;
font-size: 12px;
color: #cccccc;
line-height: 20px;
}
.chat-main .load {
text-align: center;
font-size: 12px;
color: #d02129;
line-height: 20px;
cursor: pointer;
}
.history-loading {
width: 100%;
text-align: center;
}
.time-tips {
color: #999;
text-align: center;
font-size: 12px;
}
.message-list {
padding: 0 10px;
}
.message-item {
display: flex;
}
.message-item-checkbox {
height: 50px;
margin-right: 15px;
display: flex;
align-items: center;
}
.input-checkbox {
position: relative;
}
.message-item-checkbox input[type='checkbox']::before,
.message-item-checkbox input[type='checkbox']:checked::before {
content: '';
position: absolute;
top: -3px;
left: -3px;
background: #ffffff;
width: 18px;
height: 18px;
border: 1px solid #cccccc;
border-radius: 50%;
}
.message-item-checkbox input[type='checkbox']:checked::before {
content: '\2713';
background-color: #d02129;
width: 18px;
color: #ffffff;
text-align: center;
font-weight: bold;
}
.message-item-content {
flex: 1;
margin: 5px 0;
overflow: hidden;
display: flex;
}
.sender-info {
margin: 0 5px;
}
.sender-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.message-content {
max-width: calc(100% - 100px);
}
.message-payload {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.pending {
background: url('@/assets/images/pending.gif') no-repeat center;
background-size: 13px;
width: 15px;
height: 15px;
}
.send-fail {
background: url('@/assets/images/failed.png') no-repeat center;
background-size: 15px;
width: 18px;
height: 18px;
margin-right: 3px;
}
.message-read {
color: gray;
font-size: 12px;
text-align: end;
height: 16px;
}
.message-unread {
color: #d02129;
font-size: 12px;
text-align: end;
height: 16px;
}
.content-text {
display: flex;
align-items: center;
text-align: left;
background: #eeeeee;
font-size: 14px;
font-weight: 500;
padding: 6px 8px;
margin: 3px 0;
line-height: 25px;
white-space: pre-line;
overflow-wrap: anywhere;
border-radius: 8px;
word-break: break-all;
}
.content-image {
display: block;
cursor: pointer;
}
.content-image img {
height: 100%;
}
.content-audio {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.content-audio .audio-facade {
min-width: 12px;
background: #eeeeee;
border-radius: 7px;
display: flex;
font-size: 14px;
padding: 8px;
margin: 5px 0;
line-height: 25px;
cursor: pointer;
}
.content-audio .audio-facade-bg {
background: url('../../assets/images/voice.png') no-repeat center;
background-size: 15px;
width: 20px;
}
.content-audio .audio-facade-bg.play-icon {
background: url('../../assets/images/play.gif') no-repeat center;
background-size: 20px;
}
.content-order {
border-radius: 5px;
border: 1px solid #eeeeee;
padding: 8px;
display: flex;
flex-direction: column;
}
.content-order .order-id {
font-size: 12px;
color: #666666;
margin-bottom: 5px;
}
.content-order .order-body {
display: flex;
font-size: 13px;
padding: 5px;
}
.content-order .order-img {
width: 70px;
height: 70px;
border-radius: 5px;
}
.content-order .order-name {
margin-left: 10px;
width: 135px;
color: #606164;
}
.content-order .order-count {
font-size: 12px;
color: #666666;
flex: 1;
}
.content-file {
width: 240px;
height: 65px;
font-size: 15px;
background: #ffffff;
border: 1px solid #eeeeee;
display: flex;
align-items: center;
padding: 10px;
border-radius: 5px;
cursor: pointer;
}
.content-file:hover {
background: #f1f1f1;
}
.file-info {
width: 194px;
text-align: left;
}
.file-name {
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
word-break: break-all;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.file-size {
font-size: 12px;
color: #ccc;
}
.file-img {
width: 50px;
height: 50px;
}
.message-item .self {
overflow: hidden;
display: flex;
justify-content: flex-start;
flex-direction: row-reverse;
}
.message-item .self .audio-facade {
flex-direction: row-reverse;
}
.message-item .self .audio-facade-bg {
background: url('../assets/images/voice.png') no-repeat center;
background-size: 15px;
width: 20px;
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.message-item .self .play-icon {
background: url('../assets/images/play.gif') no-repeat center;
background-size: 20px;
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);
}
.message-recalled {
display: flex;
align-items: center;
justify-content: center;
line-height: 28px;
font-size: 13px;
text-align: center;
color: grey;
margin-top: 10px;
}
.message-recalled-self {
display: flex;
}
.message-recalled-self span {
margin-left: 5px;
color: #d02129;
cursor: pointer;
}
.chat-footer {
border-top: 1px solid #dcdfe6;
width: 100%;
height: 150px;
background: #ffffff;
border-right: 1px solid #dcdfe6;
}
.action-delete {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background-color: #ffffff;
}
.delete-btn {
width: 25px;
height: 25px;
padding: 10px;
background: #f5f5f5;
border-radius: 50%;
cursor: pointer;
margin-bottom: 10px;
}
.action-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.action-bar {
display: flex;
flex-direction: row;
padding: 0 10px;
}
.action-bar .action-item {
text-align: left;
// padding: 10px 0;
position: relative;
}
.last-action-item {
display: flex;
align-items: center;
float: right;
position: relative;
left: 342px;
cursor: pointer;
/* color: #3760f1; */
}
.svg-icon {
margin-right: 4px;
}
.action-bar .action-item .iconfont {
font-size: 22px;
margin: 0 10px;
z-index: 3;
color: #606266;
cursor: pointer;
}
.action-bar .action-item .iconfont:focus {
outline: none;
}
.action-bar .action-item .iconfont:hover {
color: #d02129;
}
.emoji-box {
width: 284px;
position: absolute;
top: -147px;
left: -10px;
z-index: 2007;
background: #fff;
border: 1px solid #ebeef5;
padding: 12px;
text-align: justify;
font-size: 14px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
word-break: break-all;
border-radius: 4px;
}
.emoji-item {
width: 24px;
height: 24px;
margin: 3px 4px;
display: inline-block;
}
.input-box {
padding: 0 10px;
flex: 1;
}
.input-content {
border: none;
resize: none;
display: block;
padding: 5px 15px;
box-sizing: border-box;
width: 100%;
height: 70px;
color: #606266;
outline: none;
background: #ffffff;
word-break: break-all;
}
.send-box {
padding: 5px 10px 10px 10px;
text-align: right;
}
.send-button {
width: 70px;
height: 30px;
font-size: 15px;
border: 1px solid #d02129;
background-color: #ffffff;
color: #d02129;
border-radius: 5px;
}
.action-popup {
width: 851px;
height: 100%;
position: absolute;
top: 0;
left: -281px;
background: rgba(51, 51, 51, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.action-popup-main {
width: 150px;
height: 120px;
background: #ffffff;
z-index: 100;
border-radius: 10px;
overflow: hidden;
}
.action-popup-main .action-item {
text-align: center;
line-height: 40px;
font-size: 15px;
color: #262628;
border-bottom: 1px solid #efefef;
cursor: pointer;
}
.image-preview {
max-width: 750px;
max-height: 500px;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
position: fixed;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 9998;
}
.image-preview img {
max-width: 750px;
max-height: 500px;
}
.image-preview .close {
font-size: 50px;
line-height: 24px;
cursor: pointer;
color: #ffffff;
position: absolute;
top: 10px;
right: 5px;
z-index: 1002;
}
.order-box {
width: 848px;
position: absolute;
left: -281px;
right: 0;
top: 0;
bottom: 0;
z-index: 2007;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(33, 33, 33, 0.7);
}
.order-list {
width: 300px;
background: #f1f1f1;
border-radius: 5px;
}
.order-list .title {
font-weight: 600;
font-size: 15px;
color: #000000;
margin-left: 10px;
margin-right: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.order-list .title span {
font-size: 28px;
font-weight: 400;
cursor: pointer;
}
.order-list .order-item {
padding: 10px;
background: #ffffff;
margin: 10px;
border-radius: 5px;
cursor: pointer;
}
.order-list .order-id {
font-size: 12px;
color: #666666;
margin-bottom: 5px;
}
.order-list .order-body {
display: flex;
font-size: 13px;
justify-content: space-between;
}
.order-list .order-img {
width: 50px;
height: 50px;
border-radius: 5px;
}
.order-list .order-name {
width: 160px;
}
.order-list .order-count {
font-size: 12px;
color: #666666;
flex: 1;
}
.home-record {
/* border: 1px red solid; */
width: 318px;
/* padding: ; */
}
.home-record-title {
height: 48px;
padding: 0;
display: flex;
align-items: center;
font-size: 18px;
border-bottom: 1px solid #dcdfe6;
}
.home-record-tabList {
width: 100%;
display: flex;
}
:deep(.el-tabs__nav) {
margin-left: 60px;
}
:deep(.el-tabs__nav-wrap) {
width: 318px;
border-right: 1px solid #dcdfe6;
}
:deep(.el-tabs) {
--el-tabs-header-height: 48px;
}
:deep(.el-tabs__content) {
padding: 0 10px;
}
.messageHistory-list {
// border: 1px red solid;
height: 530px;
.messageHistory-list-item {
// height: 50px;
padding: 10px 0;
.messageHistory-list-item-avatar {
height: 30px;
img {
width: 40px;
height: 40px;
}
margin-right: 10px;
}
.messageHistory-list-item-name {
font-size: 10px;
color: #64748b;
margin-bottom: 2px;
span {
float: right;
margin-right: 10px;
}
}
.messageHistory-list-item-content-text {
display: block;
}
.messageHistory-list-item-right {
width: 100%;
padding-bottom: 10px;
border-bottom: 1px #ebeef5 solid;
}
}
}
:deep(.scrollbar-wrapper) {
overflow-x: hidden !important;
background-color: #fff;
}
.accept-message {
width: 100%;
text-align: center;
color: #606164;
line-height: 25px;
}
</style>