实现IM系统与PC端一致的WebSocket连接,优化聊天功能,支持多端消息同步,更新联系人和会话列表接口,增强用户体验。
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { randomString } from './uitls'
|
import md5 from 'md5'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
|
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
|
||||||
@@ -8,66 +8,81 @@ class socket {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.socketUrl = baseUrl
|
this.socketUrl = baseUrl
|
||||||
this.socketStart = false
|
this.socketStart = false
|
||||||
this.socketType = ''
|
this.socketType = 'websocket'
|
||||||
|
this._heartbeatTimer = null
|
||||||
this.monitorSocketError()
|
this.monitorSocketError()
|
||||||
this.monitorSocketClose()
|
this.monitorSocketClose()
|
||||||
this.socketReceive()
|
this.socketReceive()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】系统 WebSocket 地址与 PC 一致-----------
|
||||||
|
/** 构建系统 WebSocket 地址:/websocket/{userId}_{md5(token)} */
|
||||||
|
buildSystemWebSocketUrl() {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const userId = userStore.userInfo.userid
|
||||||
|
const token = userStore.userInfo.token || ''
|
||||||
|
if (!userId || !this.socketUrl) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const wsClientId = md5(token)
|
||||||
|
const base = this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://').replace(/\/$/, '')
|
||||||
|
return `${base}/websocket/${userId}_${wsClientId}`
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】系统 WebSocket 地址与 PC 一致-----------
|
||||||
|
|
||||||
init(socket_type, callback?) {
|
init(socket_type, callback?) {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
if (baseUrl) {
|
if (!baseUrl) {
|
||||||
if (this.socketStart) {
|
|
||||||
console.log('webSocket已经启动了')
|
|
||||||
} else {
|
|
||||||
this.socketType = socket_type
|
|
||||||
let url =
|
|
||||||
this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://') +
|
|
||||||
'/' +
|
|
||||||
socket_type +
|
|
||||||
'/' +
|
|
||||||
userStore.userInfo.userid +
|
|
||||||
'_app'
|
|
||||||
if (socket_type == 'eoaNewChatSocket') {
|
|
||||||
let randomMessageId = randomString(6)
|
|
||||||
url =
|
|
||||||
this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://') +
|
|
||||||
'/eoaNewChatSocket/' +
|
|
||||||
userStore.userInfo.userid +
|
|
||||||
'/' +
|
|
||||||
randomMessageId
|
|
||||||
}
|
|
||||||
console.log('启动this.socketUrl连接地址:', url)
|
|
||||||
// update-begin-author:taoyan date:20220422 for:v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
|
|
||||||
|
|
||||||
let token = userStore.userInfo.token
|
|
||||||
uni.connectSocket({
|
|
||||||
url: url,
|
|
||||||
method: 'GET',
|
|
||||||
protocols: [token],
|
|
||||||
})
|
|
||||||
// update-end-author:taoyan date:20220422 for: v2.4.6 的 websocket 服务端,存在性能和安全问题。 #3278
|
|
||||||
uni.onSocketOpen((res) => {
|
|
||||||
this.socketStart = true
|
|
||||||
callback && callback()
|
|
||||||
console.log('WebSocket连接已打开!')
|
|
||||||
})
|
|
||||||
/* setTimeout(() => {
|
|
||||||
this.getHeartbeat();
|
|
||||||
}, 5000); */
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('config/baseUrl socketUrl为空')
|
console.log('config/baseUrl socketUrl为空')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
if (this.socketStart) {
|
||||||
|
console.log('webSocket已经启动了')
|
||||||
|
callback && callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.socketType = socket_type || 'websocket'
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】连接原生 /websocket-----------
|
||||||
|
let url = this.buildSystemWebSocketUrl()
|
||||||
|
if (this.socketType !== 'websocket') {
|
||||||
|
// 兼容旧路径(如历史业务仍传入自定义 type)
|
||||||
|
url =
|
||||||
|
this.socketUrl.replace('https://', 'wss://').replace('http://', 'ws://') +
|
||||||
|
'/' +
|
||||||
|
this.socketType +
|
||||||
|
'/' +
|
||||||
|
userStore.userInfo.userid +
|
||||||
|
'_app'
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】连接原生 /websocket-----------
|
||||||
|
if (!url) {
|
||||||
|
console.log('WebSocket url 为空,跳过连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('启动this.socketUrl连接地址:', url)
|
||||||
|
const token = userStore.userInfo.token
|
||||||
|
uni.connectSocket({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
protocols: [token],
|
||||||
|
})
|
||||||
|
uni.onSocketOpen((res) => {
|
||||||
|
this.socketStart = true
|
||||||
|
callback && callback()
|
||||||
|
console.log('WebSocket连接已打开!')
|
||||||
|
this.startHeartbeat()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket给服务器发送消息
|
// Socket给服务器发送消息
|
||||||
send(data, callback) {
|
send(data, callback) {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
if (userStore.userInfo.userid) {
|
if (userStore.userInfo.userid && typeof data === 'object' && data !== null) {
|
||||||
data.userUid = userStore.userInfo.userid
|
data.userUid = userStore.userInfo.userid
|
||||||
}
|
}
|
||||||
console.log(data)
|
const payload = typeof data === 'string' ? data : JSON.stringify(data)
|
||||||
uni.sendSocketMessage({
|
uni.sendSocketMessage({
|
||||||
data: JSON.stringify(data),
|
data: payload,
|
||||||
success: () => {
|
success: () => {
|
||||||
callback && callback(true)
|
callback && callback(true)
|
||||||
},
|
},
|
||||||
@@ -76,61 +91,76 @@ class socket {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptMessage(msg) {
|
acceptMessage(msg) {
|
||||||
console.log(msg)
|
console.log(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Socket接收服务器发送过来的消息
|
// Socket接收服务器发送过来的消息
|
||||||
socketReceive() {
|
socketReceive() {
|
||||||
uni.onSocketMessage((res) => {
|
uni.onSocketMessage((res) => {
|
||||||
console.log('APP:--》收到服务器内容:')
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】忽略心跳并解析 JSON-----------
|
||||||
let data = JSON.parse(res.data)
|
if (res.data === 'ping' || res.data === 'pong') {
|
||||||
// console.log('收到服务器内容:', data);
|
return
|
||||||
|
}
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||||||
|
} catch (e) {
|
||||||
|
console.log('WebSocket 非 JSON 消息:', res.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】忽略心跳并解析 JSON-----------
|
||||||
this.acceptMessage && this.acceptMessage(data)
|
this.acceptMessage && this.acceptMessage(data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭Socket
|
// 关闭Socket
|
||||||
closeSocket() {
|
closeSocket() {
|
||||||
|
this.stopHeartbeat()
|
||||||
uni.closeSocket()
|
uni.closeSocket()
|
||||||
this.socketStart = false
|
this.socketStart = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听Socket关闭
|
// 监听Socket关闭
|
||||||
monitorSocketClose() {
|
monitorSocketClose() {
|
||||||
uni.onSocketClose((res) => {
|
uni.onSocketClose((res) => {
|
||||||
console.log('WebSocket 已关闭!')
|
console.log('WebSocket 已关闭!')
|
||||||
this.socketStart = false
|
this.socketStart = false
|
||||||
|
this.stopHeartbeat()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.init(this.socketType)
|
this.init(this.socketType)
|
||||||
}, 3000)
|
}, 3000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听Socket错误
|
// 监听Socket错误
|
||||||
monitorSocketError() {
|
monitorSocketError() {
|
||||||
uni.onSocketError(function (res) {
|
uni.onSocketError((res) => {
|
||||||
this.socketStart = false
|
this.socketStart = false
|
||||||
console.log('WebSocket连接打开失败,请检查!')
|
console.log('WebSocket连接打开失败,请检查!')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 心跳
|
|
||||||
getHeartbeat() {
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】心跳 ping-----------
|
||||||
const userStore = useUserStore()
|
startHeartbeat() {
|
||||||
this.send(
|
this.stopHeartbeat()
|
||||||
{
|
this._heartbeatTimer = setInterval(() => {
|
||||||
type: '心跳',
|
if (!this.socketStart) {
|
||||||
userUid: userStore.userInfo.userid,
|
return
|
||||||
},
|
}
|
||||||
(val) => {
|
this.send('ping')
|
||||||
setTimeout(() => {
|
}, 55000)
|
||||||
if (val) {
|
|
||||||
// this.getHeartbeat();
|
|
||||||
} else {
|
|
||||||
if (!this.socketStart) {
|
|
||||||
// this.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 10000)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this._heartbeatTimer) {
|
||||||
|
clearInterval(this._heartbeatTimer)
|
||||||
|
this._heartbeatTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】心跳 ping-----------
|
||||||
}
|
}
|
||||||
|
|
||||||
const mySocket = new socket()
|
const mySocket = new socket()
|
||||||
export default mySocket
|
export default mySocket
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
style: {
|
style: {
|
||||||
navigationBarTitleText: '聊天',
|
navigationBarTitleText: '聊天',
|
||||||
navigationStyle: 'custom',
|
navigationStyle: 'custom',
|
||||||
disableScroll: true, // 微信禁止页面滚动
|
disableScroll: true,
|
||||||
'app-plus': {
|
'app-plus': {
|
||||||
bounce: 'none', // 禁用 iOS 弹性效果
|
bounce: 'none',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
>
|
>
|
||||||
<view class="wrap">
|
<view class="wrap">
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<z-paging ref="paging" v-model="dataList" :fixed="false" use-chat-record-mode use-virtual-list cell-height-mode="dynamic" safe-area-inset-bottom bottom-bg-color="#e5e5e5" @query="queryList" @keyboardHeightChange="keyboardHeightChange" @hidedKeyboard="hidedKeyboard">
|
<z-paging ref="paging" v-model="dataList" :fixed="false" :auto="false" use-chat-record-mode use-virtual-list cell-height-mode="dynamic" safe-area-inset-bottom bottom-bg-color="#e5e5e5" @query="queryList" @keyboardHeightChange="keyboardHeightChange" @hidedKeyboard="hidedKeyboard">
|
||||||
<template #cell="{item,index}">
|
<template #cell="{item,index}">
|
||||||
<view style="transform: scaleY(-1)">
|
<view style="transform: scaleY(-1)">
|
||||||
<chat-item :item="item" :playMsgid="playMsgid" @playVoice="handlePlayVoice"></chat-item>
|
<chat-item :item="item" :playMsgid="playMsgid" @playVoice="handlePlayVoice"></chat-item>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
<template #bottom>
|
<template #bottom>
|
||||||
<chat-input-bar ref="inputBar" @send="doSend" />
|
<chat-input-bar ref="inputBar" @send="doSend" @image="handleImage" />
|
||||||
</template>
|
</template>
|
||||||
</z-paging>
|
</z-paging>
|
||||||
</view>
|
</view>
|
||||||
@@ -38,354 +38,230 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
//
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
|
|
||||||
import { nextTick, onMounted, ref } from 'vue'
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { http } from '@/utils/http'
|
import { useToast } from 'wot-design-uni'
|
||||||
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
|
|
||||||
import { useRouter } from '@/plugin/uni-mini-router'
|
import { useRouter } from '@/plugin/uni-mini-router'
|
||||||
import { cache, getFileAccessHttpUrl, hasRoute, formatDate } from '@/common/uitls'
|
import { formatDate } from '@/common/uitls'
|
||||||
import { TENANT_LIST } from '@/common/constants'
|
|
||||||
import socket from '@/common/socket'
|
import socket from '@/common/socket'
|
||||||
import { textReplaceEmoji, getEmojiImageUrl } from './emojis'
|
import { textReplaceEmoji } from './emojis'
|
||||||
import chatInputBar from './components/chat-input-bar.vue'
|
import chatInputBar from './components/chat-input-bar.vue'
|
||||||
import chatItem from './components/chat-item.vue'
|
import chatItem from './components/chat-item.vue'
|
||||||
import { getEnvBaseUrl } from '@/utils/index'
|
|
||||||
import { useParamsStore } from '@/store/page-params'
|
import { useParamsStore } from '@/store/page-params'
|
||||||
|
import {
|
||||||
|
fetchMessages,
|
||||||
|
markRead,
|
||||||
|
openConversation,
|
||||||
|
sendMessage,
|
||||||
|
mapMessageToBubble,
|
||||||
|
type ImMessage,
|
||||||
|
} from '@/service/im/chat'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
options: {
|
options: {
|
||||||
// apply-shared:当前页面样式会影响到子组件样式.(小程序)
|
|
||||||
// shared:当前页面样式影响到子组件,子组件样式也会影响到当前页面.(小程序)
|
|
||||||
styleIsolation: 'apply-shared',
|
styleIsolation: 'apply-shared',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const api = {
|
|
||||||
chatlog_old: '/eoa/im/api/queryChatLogList',
|
|
||||||
chatlog: '/eoa/im/newApi/records',
|
|
||||||
sendMsg: '/eoa/im/newApi/sendMessage',
|
|
||||||
sendFile: '/eoa/im/newApi/sendFile',
|
|
||||||
creatFriendSession: '/eoa/im/newApi/creatFriendSession',
|
|
||||||
uploadUrl: `${getEnvBaseUrl()}/eoa/im/newApi/sendImage`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const paging = ref(null)
|
const paging = ref(null)
|
||||||
|
|
||||||
// 信息
|
const chatObj = ref<any>(null)
|
||||||
const chatObj = ref(null)
|
const conversationId = ref('')
|
||||||
// 对方userid 或者 群id 或者 讨论组id
|
|
||||||
const chatto = ref()
|
const chatto = ref()
|
||||||
const navTitle = ref('')
|
const navTitle = ref('')
|
||||||
const myuid = ref(userStore.userInfo.userid)
|
const myuid = ref(userStore.userInfo.userid)
|
||||||
const msgList = ref([])
|
|
||||||
// const pageNo = ref(1)
|
|
||||||
// const pageSize = ref(10)
|
|
||||||
const loadingShow = ref(false)
|
|
||||||
const hasRecord = ref(false)
|
|
||||||
const dataList = ref([])
|
const dataList = ref([])
|
||||||
const inputBar = ref(null)
|
const inputBar = ref(null)
|
||||||
const AUDIO = uni.createInnerAudioContext()
|
const AUDIO = uni.createInnerAudioContext()
|
||||||
const playMsgid = ref('')
|
const playMsgid = ref('')
|
||||||
let stopWatch: any = null
|
|
||||||
const paramsStore = useParamsStore()
|
const paramsStore = useParamsStore()
|
||||||
const backRouteName = ref('message')
|
const backRouteName = ref('message')
|
||||||
const routeMethod = ref('pushTab')
|
const routeMethod = ref('pushTab')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// 是否首次加载
|
|
||||||
let isFirstLoad = true
|
let isFirstLoad = true
|
||||||
let chatItemData = {}
|
let chatItemData: any = {}
|
||||||
|
/** 已处理的消息 ID,防重复插入 */
|
||||||
|
const processedMsgIds = new Set<string>()
|
||||||
|
|
||||||
// 页面初始化
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】聊天页接真实接口-----------
|
||||||
const init = () => {
|
const init = async () => {
|
||||||
const localData = paramsStore.getPageParams('chat')
|
const localData = paramsStore.getPageParams('chat')
|
||||||
const params = localData.data
|
const params = localData?.data
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
chatObj.value = { ...params }
|
chatObj.value = { ...params }
|
||||||
// 对方头像、群头像、讨论组头像
|
|
||||||
navTitle.value = params.fromUserName
|
navTitle.value = params.fromUserName
|
||||||
// 对方userid 或者 群id 或者 讨论组id
|
chatto.value = params.msgTo || params.targetUserId
|
||||||
chatto.value = chatObj.value.msgTo
|
|
||||||
chatItemData = params
|
chatItemData = params
|
||||||
if (params.type == 'friend') {
|
conversationId.value = params.conversationId || ''
|
||||||
// 创建会话数据
|
|
||||||
creatFriendSession(chatObj.value.msgTo)
|
// 从联系人进入且无会话:先 open
|
||||||
|
if (!conversationId.value && params.type === 'friend' && (params.targetUserId || params.msgTo)) {
|
||||||
|
const targetUserId = params.targetUserId || params.msgTo
|
||||||
|
try {
|
||||||
|
const res: any = await openConversation(targetUserId)
|
||||||
|
if (res.success && res.result?.conversationId) {
|
||||||
|
conversationId.value = res.result.conversationId
|
||||||
|
chatObj.value.conversationId = conversationId.value
|
||||||
|
chatItemData.conversationId = conversationId.value
|
||||||
|
if (res.result.targetRealname) {
|
||||||
|
navTitle.value = res.result.targetRealname
|
||||||
|
chatObj.value.fromUserName = res.result.targetRealname
|
||||||
|
}
|
||||||
|
if (res.result.targetAvatar) {
|
||||||
|
chatObj.value.fromAvatar = res.result.targetAvatar
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.warning(res.message || '打开会话失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.warning('打开会话失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 启动webSocket
|
|
||||||
onSocketOpen()
|
onSocketOpen()
|
||||||
// 接收消息
|
|
||||||
onSocketReceive()
|
onSocketReceive()
|
||||||
// 加载初始页面消息
|
doMarkRead()
|
||||||
getMsgList()
|
|
||||||
if (localData.back) {
|
if (localData.back) {
|
||||||
backRouteName.value = localData.back
|
backRouteName.value = localData.back
|
||||||
routeMethod.value = localData.routeMethod
|
routeMethod.value = localData.routeMethod
|
||||||
}
|
}
|
||||||
}
|
// 会话就绪后再拉消息,避免与异步 open 竞态
|
||||||
// 创建会话数据
|
nextTick(() => {
|
||||||
const creatFriendSession = (userId) => {
|
paging.value?.reload()
|
||||||
http.post(api.creatFriendSession, {
|
|
||||||
type: 'friend',
|
|
||||||
userId,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doMarkRead = () => {
|
||||||
|
if (!conversationId.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
markRead(conversationId.value).then(() => {
|
||||||
|
uni.$emit('chatList:unreadClear', chatItemData)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
const onSocketOpen = () => {
|
const onSocketOpen = () => {
|
||||||
// console.log('启动webSocket')
|
socket.init('websocket')
|
||||||
// socket.init('eoaNewChatSocket')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSocketReceive = () => {
|
const onSocketReceive = () => {
|
||||||
var _this = this
|
|
||||||
socket.acceptMessage = function (res) {
|
socket.acceptMessage = function (res) {
|
||||||
console.log('页面收到的消息=====》', res)
|
if (res?.cmd !== 'chat') {
|
||||||
if (res.event == 'event_talk_revoke') {
|
return
|
||||||
// 撤回了消息
|
|
||||||
removeMsg(res)
|
|
||||||
} else {
|
|
||||||
// event_chat_talk
|
|
||||||
if (res.event.startsWith('event_chat')) {
|
|
||||||
//聊天消息
|
|
||||||
screenMsg(res)
|
|
||||||
unreadClear()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (res.conversationId !== conversationId.value) {
|
||||||
}
|
return
|
||||||
const removeMsg = (data) => {
|
}
|
||||||
let arr = msgList.value.filter((item) => item.id != data.id)
|
const msgId = res.messageId
|
||||||
msgList.value = arr
|
//update-begin---author:xsl ---date:2026-07-20 for:【IM多端同步】WS 与本地发送竞态去重-----------
|
||||||
}
|
if (msgId && processedMsgIds.has(msgId)) {
|
||||||
const screenMsg = (msg) => {
|
return
|
||||||
// 消息处理
|
}
|
||||||
let reuslt = false
|
if (msgId && dataList.value.some((item: any) => item.id === msgId)) {
|
||||||
if (chatObj.value.type == 'friend') {
|
processedMsgIds.add(msgId)
|
||||||
reuslt = msg.data.msgFrom == chatto.value && msg.data.msgTo == myuid.value
|
setTimeout(() => processedMsgIds.delete(msgId), 5000)
|
||||||
} else if (['group', 'discussion'].includes(chatObj.value.type)) {
|
return
|
||||||
reuslt = msg.data.msgFrom !== myuid.value && msg.data.msgTo == chatto.value
|
}
|
||||||
}
|
if (msgId) {
|
||||||
if (reuslt) {
|
processedMsgIds.add(msgId)
|
||||||
console.log('用户消息')
|
setTimeout(() => processedMsgIds.delete(msgId), 5000)
|
||||||
let time = formatDate(msg.data.sendTime, 'yyyy-MM-dd hh:mm:ss')
|
}
|
||||||
let id = time.replace(/\:/g, '').replace(/\-/g, '').replace(' ', '')
|
//update-end---author:xsl ---date:2026-07-20 for:【IM多端同步】WS 与本地发送竞态去重-----------
|
||||||
paging.value?.addChatRecordData(
|
const bubble = mapMessageToBubble({
|
||||||
analysis([
|
id: res.messageId,
|
||||||
{
|
conversationId: res.conversationId,
|
||||||
fromUserName: msg.data.fromUserName,
|
senderId: res.senderId,
|
||||||
msgTo: msg.data.msgTo,
|
senderName: res.senderName,
|
||||||
msgFrom: msg.data.msgFrom,
|
senderAvatar: res.senderAvatar,
|
||||||
msgData: msg.data.msgData,
|
content: res.content,
|
||||||
fromAvatar: msg.data.fromAvatar,
|
msgType: res.msgType || 'text',
|
||||||
sendTime: msg.data.sendTime,
|
createTime: formatWsTime(res.createTime),
|
||||||
msgType: msg.data.msgType,
|
})
|
||||||
sendTimeId: id,
|
paging.value?.addChatRecordData(analysis([bubble]))
|
||||||
fileName: msg.data.fileName,
|
if (res.senderId !== myuid.value) {
|
||||||
id: msg.data.id,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
//非自己的消息震动
|
|
||||||
if (msg.msgFrom != myuid.value) {
|
|
||||||
console.log('振动')
|
|
||||||
uni.vibrateLong()
|
uni.vibrateLong()
|
||||||
|
doMarkRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//替换表情符号为图片
|
|
||||||
|
const formatWsTime = (t: any) => {
|
||||||
|
if (!t) {
|
||||||
|
return formatDate(new Date().getTime(), 'yyyy-MM-dd hh:mm:ss')
|
||||||
|
}
|
||||||
|
if (typeof t === 'string') {
|
||||||
|
return t.length >= 19 ? t.substring(0, 19) : t
|
||||||
|
}
|
||||||
|
return formatDate(new Date(t).getTime(), 'yyyy-MM-dd hh:mm:ss')
|
||||||
|
}
|
||||||
|
|
||||||
const replaceEmoji = (str) => {
|
const replaceEmoji = (str) => {
|
||||||
let temp = textReplaceEmoji(str)
|
const temp = textReplaceEmoji(str)
|
||||||
return '<div style="display:inline-block">' + temp + '</div>'
|
return '<div style="display:inline-block">' + temp + '</div>'
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryList = (pageNo, pageSize) => {
|
const queryList = (pageNo, pageSize) => {
|
||||||
//数据库查询消息列表
|
if (!conversationId.value) {
|
||||||
let params = {
|
paging.value.complete([])
|
||||||
type: chatObj.value.type,
|
return
|
||||||
pageNo: pageNo,
|
|
||||||
pageSize: pageSize,
|
|
||||||
msgTo: chatto.value,
|
|
||||||
// id: myuid.value,
|
|
||||||
// sort: 'DESC',
|
|
||||||
}
|
}
|
||||||
console.log('params', params)
|
fetchMessages({
|
||||||
const data = [
|
conversationId: conversationId.value,
|
||||||
{
|
pageNo,
|
||||||
id: 1,
|
pageSize,
|
||||||
fromUserName: '顾平',
|
})
|
||||||
sendTime: '1977-11-13',
|
.then((res: any) => {
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
if (!res.success) {
|
||||||
type: 'friend',
|
toast.warning(res.message || '加载消息失败')
|
||||||
izTop: 1,
|
paging.value.complete(false)
|
||||||
status: 'offline',
|
return
|
||||||
msgFrom: 4000,
|
|
||||||
msgTo: 100,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'可半达办外将物起算置知空子。上题务点条界思清法导集马为和计。始在形计各强可求开去手先下识极级育专。形子民委想己给号全维精道应。斯适般不拉个世被资提达须之多关。己除约确元则次同风平维音毛听。少验且内果论治量军们活展观情面元越。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
fromUserName: '熊艳',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'friend',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'online',
|
|
||||||
msgFrom: 4012,
|
|
||||||
msgTo: 134,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'斗取写也展需会于儿次只三自到界民品因。只代装细打三管规这前千器她她入音即准。必象和长使南资几时明因米多交极须空。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
fromUserName: '康娜',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 1,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4024,
|
|
||||||
msgTo: 168,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'图步把单增利老何列力道何认象。子各交群容产识一界边先式声。被思务共是圆音少际指王元压没任内共。养作包京何铁历属信战族再线理却来情料。能周内家术向建满思书温音太装。时她切调它论族温中三关且与志千。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
fromUserName: '石平',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'group',
|
|
||||||
izTop: 1,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4036,
|
|
||||||
msgTo: 202,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'民高价般直素划达期矿身更价律或支今。争局便工争相算则参调信斯。把清得完别青院阶火老先位包回速变。意温太合为这来物只我非象专型又加军。至取拉方毛眼例着大你这每相亲该元风。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
fromUserName: '马杰',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'group',
|
|
||||||
izTop: 1,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4048,
|
|
||||||
msgTo: 236,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'除命市与命成型两口接在及民志提能。需参处空容感学长革段易给分。始真结红型图处技界党非青机山。广直声从老照如写中么省风但。论斗展就积信区北形大报达据把青要。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
fromUserName: '沈艳',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'friend',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4060,
|
|
||||||
msgTo: 270,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'规响用其江众器老也成等听节电西。系石教建还厂文气要际长中电今。情素他头资风矿象非这经格老包问过。史期带之走北形历值从所因断八技。红行没众说共史张在任千物今老。传队命做打此无直转术直其门公。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
fromUserName: '崔芳',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'online',
|
|
||||||
msgFrom: 4072,
|
|
||||||
msgTo: 304,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'识保住并非先严间眼马级点叫识只管。写信为每下数集被料前号变很整合。收业后局看并太能决来二府展建片活即体。给史求音很三动作目重因质除提。法关活量管集求公又再时共小明个自确。集提支很也规入并我基照计最要飞院面。',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
fromUserName: '郝超',
|
|
||||||
sendTime: '1977-11-13',
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 1,
|
|
||||||
status: 'online',
|
|
||||||
msgFrom: 4084,
|
|
||||||
msgTo: 338,
|
|
||||||
userId: '1678948772039729154',
|
|
||||||
msgType: 'text',
|
|
||||||
msgData:
|
|
||||||
'状断只派器新以真业强说部多确料。始矿认要联清才权况况法色式。引办研角且百国路里还计走中细位。或温作经人周复技常位交文共没运。',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const records = analysis(data)
|
|
||||||
paging.value.complete(records)
|
|
||||||
if (isFirstLoad) {
|
|
||||||
uni.$emit('chatList:unreadClear', chatItemData)
|
|
||||||
unreadClear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const analysis = (data) => {
|
|
||||||
let arr = data
|
|
||||||
if (arr.length > 0) {
|
|
||||||
let list = arr.map((item) => {
|
|
||||||
let id = String(item.sendTime).replace(/\:/g, '').replace(/\-/g, '').replace(' ', '')
|
|
||||||
item.sendTimeId = id
|
|
||||||
let content = item.msgData
|
|
||||||
if (item.msgType == 'text') {
|
|
||||||
content = replaceEmoji(content)
|
|
||||||
}
|
}
|
||||||
if (item.msgType == 'voice') {
|
const records: ImMessage[] = res.result?.records || []
|
||||||
content = JSON.parse(content)
|
// z-paging 聊天模式:数据需从新到旧
|
||||||
|
const newestFirst = [...records].reverse()
|
||||||
|
const bubbles = analysis(newestFirst.map(mapMessageToBubble))
|
||||||
|
paging.value.complete(bubbles)
|
||||||
|
if (isFirstLoad) {
|
||||||
|
isFirstLoad = false
|
||||||
|
uni.$emit('chatList:unreadClear', chatItemData)
|
||||||
|
doMarkRead()
|
||||||
}
|
}
|
||||||
item.msgData = content
|
|
||||||
return item
|
|
||||||
})
|
})
|
||||||
for (let i = 0; i < list.length; i++) {
|
.catch(() => {
|
||||||
if (list[i].msgType == 'revoke') {
|
paging.value.complete(false)
|
||||||
continue
|
})
|
||||||
}
|
|
||||||
if (list[i].referenceMsgId) {
|
|
||||||
list[i] = handleReplyMsg(list[i], list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载初始页面消息
|
const analysis = (data) => {
|
||||||
const getMsgList = () => {}
|
if (!data?.length) {
|
||||||
const handleReplyMsg = (item, list) => {
|
return data || []
|
||||||
let tempId = item.referenceMsgId
|
|
||||||
item.reply = true
|
|
||||||
let replyContent = ''
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
if (list[i].id == tempId) {
|
|
||||||
replyContent = '"' + list[i].fromUserName + ':' + list[i].msgData + '"'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
item.replyContent = replyContent
|
return data.map((item) => {
|
||||||
return item
|
const sendTime = item.sendTime || ''
|
||||||
|
item.sendTimeId = String(sendTime).replace(/:/g, '').replace(/-/g, '').replace(' ', '')
|
||||||
|
let content = item.msgData
|
||||||
|
if (item.msgType === 'text') {
|
||||||
|
content = replaceEmoji(content)
|
||||||
|
}
|
||||||
|
if (item.msgType === 'voice' && typeof content === 'string') {
|
||||||
|
try {
|
||||||
|
content = JSON.parse(content)
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
item.msgData = content
|
||||||
|
return item
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const unreadClear = () => {}
|
|
||||||
// 播放语音
|
|
||||||
const handlePlayVoice = (item) => {
|
const handlePlayVoice = (item) => {
|
||||||
if (item.id == playMsgid.value) {
|
if (item.id == playMsgid.value) {
|
||||||
AUDIO.stop()
|
AUDIO.stop()
|
||||||
@@ -398,150 +274,88 @@ const handlePlayVoice = (item) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 播放语音结束
|
AUDIO.onEnded(() => {
|
||||||
AUDIO.onEnded((res) => {
|
|
||||||
playMsgid.value = ''
|
playMsgid.value = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听键盘高度改变,请不要直接通过uni.onKeyboardHeightChange监听,否则可能导致z-paging内置的键盘高度改变监听失效(如果不需要切换表情面板则不用写)
|
|
||||||
const keyboardHeightChange = (res) => {
|
const keyboardHeightChange = (res) => {
|
||||||
inputBar.value.updateKeyboardHeightChange(res)
|
inputBar.value?.updateKeyboardHeightChange(res)
|
||||||
}
|
}
|
||||||
// 用户尝试隐藏键盘,此时如果表情面板在展示中,应当通知chatInputBar隐藏表情面板(如果不需要切换表情面板则不用写)
|
|
||||||
const hidedKeyboard = () => {
|
const hidedKeyboard = () => {
|
||||||
inputBar.value.hidedKeyboard()
|
inputBar.value?.hidedKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
const doSend = (textMsg) => {
|
const doSend = (textMsg) => {
|
||||||
let content = replaceEmoji(textMsg)
|
if (!textMsg?.trim()) {
|
||||||
//let msg = {'text':content}
|
return
|
||||||
//content = (content||'').replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&')
|
|
||||||
console.log('content', content)
|
|
||||||
let msg = textMsg
|
|
||||||
//发送
|
|
||||||
sendMsg(msg, 'text')
|
|
||||||
send({ msgData: content, msgType: 'text' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendMsg = (content, type) => {
|
|
||||||
//实际应用中,此处应该提交长连接,模板仅做本地处理。
|
|
||||||
var nowDate = new Date()
|
|
||||||
// 发送消息
|
|
||||||
var obj = {
|
|
||||||
mine: {
|
|
||||||
avatar: userStore.userInfo.avatar,
|
|
||||||
content: content,
|
|
||||||
id: myuid.value,
|
|
||||||
mine: true,
|
|
||||||
username: userStore.userInfo.username,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
avatar: chatObj.value.avatar,
|
|
||||||
id: chatObj.value.msgTo,
|
|
||||||
type: chatObj.value.type,
|
|
||||||
username: chatObj.value.username,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
if (!conversationId.value) {
|
||||||
let sendData = {
|
toast.warning('会话未就绪')
|
||||||
type: 'chatMessage',
|
return
|
||||||
data: obj,
|
|
||||||
}
|
}
|
||||||
console.log('sendData======>', sendData)
|
sendMessage({
|
||||||
let params = {
|
conversationId: conversationId.value,
|
||||||
type: chatObj.value.type,
|
content: textMsg.trim(),
|
||||||
msgTo: chatObj.value.msgTo,
|
|
||||||
text: content,
|
|
||||||
msgType: 'text',
|
msgType: 'text',
|
||||||
}
|
})
|
||||||
// http.post(api.sendMsg, params).then((res: any) => {
|
.then((res: any) => {
|
||||||
// console.log('消息发送结果:', res)
|
if (!res.success || !res.result) {
|
||||||
// if (!res.success) {
|
toast.warning(res.message || '发送失败')
|
||||||
// toast.error(res.message)
|
return
|
||||||
// }
|
}
|
||||||
// })
|
const msgId = res.result.id
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【IM多端同步】发送成功若 WS 已插入则不再本地追加-----------
|
||||||
|
if (msgId) {
|
||||||
|
processedMsgIds.add(msgId)
|
||||||
|
setTimeout(() => processedMsgIds.delete(msgId), 5000)
|
||||||
|
}
|
||||||
|
if (msgId && dataList.value.some((item: any) => item.id === msgId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【IM多端同步】发送成功若 WS 已插入则不再本地追加-----------
|
||||||
|
const bubble = mapMessageToBubble({
|
||||||
|
...res.result,
|
||||||
|
createTime: formatWsTime(res.result.createTime),
|
||||||
|
})
|
||||||
|
paging.value?.addChatRecordData(analysis([bubble]))
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.warning('发送失败')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImage = (type) => {
|
const handleImage = () => {
|
||||||
let time = formatDate(new Date().getTime(), 'yyyy-MM-dd hh:mm:ss')
|
toast.warning('图片发送暂未支持')
|
||||||
let id = time.replace(/\:/g, '').replace(/\-/g, '').replace(' ', '')
|
|
||||||
let formData = {
|
|
||||||
type: 'friend',
|
|
||||||
msgTo: chatto.value,
|
|
||||||
fileId: id,
|
|
||||||
msgType: 'images',
|
|
||||||
fileName: '',
|
|
||||||
}
|
|
||||||
const { loading, data, error, run } = useUpload(
|
|
||||||
{ ...formData, name: 'image' },
|
|
||||||
{ url: api.uploadUrl, sourceType: [type] },
|
|
||||||
)
|
|
||||||
if (stopWatch) stopWatch()
|
|
||||||
run()
|
|
||||||
stopWatch = watch(
|
|
||||||
() => [loading.value, error.value, data.value],
|
|
||||||
([loading, err, data]: any, oldValue) => {
|
|
||||||
if (loading === false) {
|
|
||||||
if (err) {
|
|
||||||
// toast.warning('修改失败')
|
|
||||||
uni.hideLoading()
|
|
||||||
} else {
|
|
||||||
if (data.code === 200) {
|
|
||||||
send({ msgData: data.result.text, msgType: 'image' })
|
|
||||||
} else {
|
|
||||||
toast.warning(data.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const send = ({ msgData, msgType }) => {
|
|
||||||
let time = formatDate(new Date().getTime(), 'yyyy-MM-dd hh:mm:ss')
|
|
||||||
let id = time.replace(/\:/g, '').replace(/\-/g, '').replace(' ', '')
|
|
||||||
//发送
|
|
||||||
paging.value.addChatRecordData(
|
|
||||||
analysis([
|
|
||||||
{
|
|
||||||
fromUserName: userStore.userInfo.realname,
|
|
||||||
msgTo: chatto.value,
|
|
||||||
msgFrom: myuid.value,
|
|
||||||
msgData: msgData,
|
|
||||||
fromAvatar: userStore.userInfo.avatar,
|
|
||||||
sendTime: time,
|
|
||||||
sendTimeId: id,
|
|
||||||
msgType,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
|
|
||||||
const handleNavBack = () => {
|
const handleNavBack = () => {
|
||||||
uni.$emit('chatList:unreadClear', chatItemData)
|
uni.$emit('chatList:unreadClear', chatItemData)
|
||||||
uni.$emit('chatList:reload')
|
uni.$emit('chatList:reload')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGoChatSetting = () => {
|
const handleGoChatSetting = () => {
|
||||||
paramsStore.setPageParams('chatSetting', {
|
toast.warning('群设置功能后续开放,请从会话列表进入群聊')
|
||||||
type: chatObj.value.type,
|
|
||||||
chatto: chatto.value,
|
|
||||||
})
|
|
||||||
router.push({
|
|
||||||
name: 'chatSetting',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
onSocketOpen()
|
||||||
|
onSocketReceive()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
uni.$on('chat:updateTile', (title) => {
|
uni.$on('chat:updateTile', (title) => {
|
||||||
navTitle.value = title
|
navTitle.value = title
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
// socket?.closeSocket()
|
|
||||||
uni.$off('chat:updateTile')
|
uni.$off('chat:updateTile')
|
||||||
})
|
})
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】聊天页接真实接口-----------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
//
|
|
||||||
.wrap {
|
.wrap {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,18 +81,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { nextTick, onMounted, ref } from 'vue'
|
import { nextTick, ref } from 'vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { http } from '@/utils/http'
|
|
||||||
import { useParamsStore } from '@/store/page-params'
|
import { useParamsStore } from '@/store/page-params'
|
||||||
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
|
import { useToast } from 'wot-design-uni'
|
||||||
import { useRouter } from '@/plugin/uni-mini-router'
|
import { useRouter } from '@/plugin/uni-mini-router'
|
||||||
import { cache, getFileAccessHttpUrl, hasRoute } from '@/common/uitls'
|
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
|
||||||
import vPinyin from '../common/vue-py'
|
import vPinyin from '../common/vue-py'
|
||||||
import rightConditionFilter from '@/components/RightConditionFilter/RightConditionFilter.vue'
|
import rightConditionFilter from '@/components/RightConditionFilter/RightConditionFilter.vue'
|
||||||
import { TENANT_LIST } from '@/common/constants'
|
import { TENANT_LIST } from '@/common/constants'
|
||||||
import defaultAvatar from '@/static/default-avatar.png'
|
import defaultAvatar from '@/static/default-avatar.png'
|
||||||
|
import { fetchDeptMembers, openConversation, mapConversationToListItem } from '@/service/im/chat'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@@ -107,23 +107,24 @@ const dataSource = ref([])
|
|||||||
const conditionFilter = reactive({ show: false, checked: '', options: [] })
|
const conditionFilter = reactive({ show: false, checked: '', options: [] })
|
||||||
const systemInfo = getApp().globalData.systemInfo
|
const systemInfo = getApp().globalData.systemInfo
|
||||||
const { height } = systemInfo.safeArea
|
const { height } = systemInfo.safeArea
|
||||||
const queryList = (pageNo, pageSize) => {
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】联系人改用 deptMembers-----------
|
||||||
const pararms = { pageNo, pageSize, tenantId: conditionFilter.checked }
|
const queryList = () => {
|
||||||
if (conditionFilter.checked === '') delete pararms.tenantId
|
fetchDeptMembers(keyword.value || undefined)
|
||||||
http
|
|
||||||
.get('/sys/user/appQueryUser', pararms)
|
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
if (res.success && res.result.length) {
|
if (res.success && Array.isArray(res.result) && res.result.length) {
|
||||||
let result = res.result
|
paging.value.complete(res.result)
|
||||||
paging.value.complete(result)
|
} else if (res.success) {
|
||||||
|
paging.value.complete([])
|
||||||
} else {
|
} else {
|
||||||
|
toast.warning(res.message || '加载联系人失败')
|
||||||
paging.value.complete(false)
|
paging.value.complete(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((res) => {
|
.catch(() => {
|
||||||
paging.value.complete(false)
|
paging.value.complete(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】联系人改用 deptMembers-----------
|
||||||
// 监听接口数据
|
// 监听接口数据
|
||||||
watch(dataList, () => {
|
watch(dataList, () => {
|
||||||
let result = handleResult(dataList.value)
|
let result = handleResult(dataList.value)
|
||||||
@@ -165,10 +166,26 @@ function handleClear() {
|
|||||||
handleSearch()
|
handleSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = (index: string, data: any) => {
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】点击联系人打开单聊-----------
|
||||||
paramsStore.setPageParams('personPage', { backRouteName: 'contacts', data })
|
const handleClick = async (_index: string, data: any) => {
|
||||||
router.push({ name: 'personPage' })
|
try {
|
||||||
|
const res: any = await openConversation(data.id)
|
||||||
|
if (!res.success || !res.result?.conversationId) {
|
||||||
|
toast.warning(res.message || '打开会话失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const listItem = mapConversationToListItem(res.result)
|
||||||
|
paramsStore.setPageParams('chat', {
|
||||||
|
back: 'contacts',
|
||||||
|
routeMethod: 'replace',
|
||||||
|
data: listItem,
|
||||||
|
})
|
||||||
|
router.push({ name: 'chat' })
|
||||||
|
} catch (e) {
|
||||||
|
toast.warning('打开会话失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】点击联系人打开单聊-----------
|
||||||
const handleChange = ({ option }) => {
|
const handleChange = ({ option }) => {
|
||||||
conditionFilter.checked = option.key
|
conditionFilter.checked = option.key
|
||||||
paging.value.reload()
|
paging.value.reload()
|
||||||
|
|||||||
@@ -158,17 +158,21 @@ const handleClick = () => {
|
|||||||
bottomOperatePopup.show = true
|
bottomOperatePopup.show = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】个人页发起单聊带 conversation 上下文-----------
|
||||||
const handleGo = () => {
|
const handleGo = () => {
|
||||||
var parmas = {
|
const parmas = {
|
||||||
fromAvatar: data.avatar,
|
fromAvatar: data.avatar,
|
||||||
fromUserName: data.realname || data.username,
|
fromUserName: data.realname || data.username,
|
||||||
msgFrom: userStore.userInfo.userid,
|
msgFrom: userStore.userInfo.userid,
|
||||||
msgTo: data.id,
|
msgTo: data.id,
|
||||||
|
targetUserId: data.id,
|
||||||
|
conversationId: data.conversationId || '',
|
||||||
type: 'friend',
|
type: 'friend',
|
||||||
}
|
}
|
||||||
paramsStore.setPageParams('chat', { back: 'personPage', routeMethod: 'replace', data: parmas })
|
paramsStore.setPageParams('chat', { back: 'personPage', routeMethod: 'replace', data: parmas })
|
||||||
router.push({ name: 'chat' })
|
router.push({ name: 'chat' })
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】个人页发起单聊带 conversation 上下文-----------
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ const queryList = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const handleGo = (item) => {
|
const handleGo = (item) => {
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】群组入口提示从会话列表进入-----------
|
||||||
|
if (item.path === 'myGroup') {
|
||||||
|
toast.warning('请从会话列表进入群聊')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】群组入口提示从会话列表进入-----------
|
||||||
if (!hasRoute({ name: item.path })) {
|
if (!hasRoute({ name: item.path })) {
|
||||||
toast.warning('还未开发~')
|
toast.warning('还未开发~')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,17 +7,11 @@
|
|||||||
:default-page-size="100"
|
:default-page-size="100"
|
||||||
>
|
>
|
||||||
<uni-list>
|
<uni-list>
|
||||||
<template v-for="(item, index) in dataList" :key="index">
|
<template v-for="(item, index) in dataList" :key="item.conversationId || index">
|
||||||
<view
|
<view
|
||||||
:class="{ list: true, isTop: item.izTop == 1 }"
|
:class="{ list: true, isTop: item.izTop == 1 }"
|
||||||
@longpress.prevent="handleLongPress(item)"
|
|
||||||
@click="handleGo(item)"
|
@click="handleGo(item)"
|
||||||
>
|
>
|
||||||
<template v-if="['systemNotice'].includes(item.type)">
|
|
||||||
<view class="avatar">
|
|
||||||
<text class="cuIcon-notice text-white"></text>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<template v-if="['group'].includes(item.type) && !item.fromAvatar">
|
<template v-if="['group'].includes(item.type) && !item.fromAvatar">
|
||||||
<view class="group avatar">
|
<view class="group avatar">
|
||||||
<text class="text-white">{{ getFirstStr(item.fromUserName) }}</text>
|
<text class="text-white">{{ getFirstStr(item.fromUserName) }}</text>
|
||||||
@@ -40,306 +34,138 @@
|
|||||||
</template>
|
</template>
|
||||||
</uni-list>
|
</uni-list>
|
||||||
</z-paging>
|
</z-paging>
|
||||||
<BottomOperate
|
|
||||||
v-if="bottomOperatePopup.show"
|
|
||||||
v-bind="bottomOperatePopup"
|
|
||||||
@close="() => (bottomOperatePopup.show = false)"
|
|
||||||
@change="handleChange"
|
|
||||||
></BottomOperate>
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
||||||
import { http } from '@/utils/http'
|
import { useToast } from 'wot-design-uni'
|
||||||
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
|
|
||||||
import { useRouter } from '@/plugin/uni-mini-router'
|
import { useRouter } from '@/plugin/uni-mini-router'
|
||||||
import { beautifyTime } from '@/common/uitls'
|
import { beautifyTime, getFileAccessHttpUrl } from '@/common/uitls'
|
||||||
import { useParamsStore } from '@/store/page-params'
|
import { useParamsStore } from '@/store/page-params'
|
||||||
import { cache, getFileAccessHttpUrl, hasRoute } from '@/common/uitls'
|
|
||||||
import socket from '@/common/socket'
|
import socket from '@/common/socket'
|
||||||
import { platform } from '@/utils/platform'
|
import { platform } from '@/utils/platform'
|
||||||
console.log('platform:::', platform)
|
import {
|
||||||
|
fetchConversations,
|
||||||
|
fetchGroups,
|
||||||
|
mapConversationToListItem,
|
||||||
|
type ImChatListItem,
|
||||||
|
} from '@/service/im/chat'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'chatList',
|
name: 'chatList',
|
||||||
options: {
|
options: {
|
||||||
styleIsolation: 'shared',
|
styleIsolation: 'shared',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const paramsStore = useParamsStore()
|
const paramsStore = useParamsStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
const paging = ref(null)
|
const paging = ref(null)
|
||||||
const avatarList = ref()
|
const dataList = ref<ImChatListItem[]>([])
|
||||||
const dataList = ref([])
|
|
||||||
const curPlatform = ref<string>(platform)
|
const curPlatform = ref<string>(platform)
|
||||||
// #ifdef APP-IOS
|
// #ifdef APP-IOS
|
||||||
curPlatform.value = 'ios'
|
curPlatform.value = 'ios'
|
||||||
// #endif
|
// #endif
|
||||||
const options = [
|
|
||||||
{ key: 'backtop', icon: 'backtop', label: '置顶' },
|
|
||||||
{ key: 'cancelbacktop', icon: 'translate-bold', label: '取消置顶' },
|
|
||||||
{ key: 'delete', icon: 'delete', label: '删除', color: 'red' },
|
|
||||||
]
|
|
||||||
const bottomOperatePopup = reactive({
|
|
||||||
show: false,
|
|
||||||
title: '',
|
|
||||||
data: {},
|
|
||||||
options: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】会话列表接真实接口-----------
|
||||||
const queryList = () => {
|
const queryList = () => {
|
||||||
const data = [
|
Promise.all([fetchConversations(), fetchGroups()])
|
||||||
{
|
.then(([singleRes, groupRes]: any[]) => {
|
||||||
id: 1,
|
if (!singleRes?.success && !groupRes?.success) {
|
||||||
fromUserName: '廖秀兰',
|
toast.warning(singleRes?.message || groupRes?.message || '加载会话失败')
|
||||||
sendTime: '2018-12-08',
|
paging.value.complete(false)
|
||||||
type: 'discussion',
|
return
|
||||||
izTop: 0,
|
}
|
||||||
status: 'offline',
|
const singles = (singleRes?.success && Array.isArray(singleRes.result) ? singleRes.result : []).map(
|
||||||
msgFrom: 4000,
|
mapConversationToListItem,
|
||||||
msgTo: 100,
|
)
|
||||||
fromAvatar: 'https://random.imagecdn.app/100/100',
|
const groups = (groupRes?.success && Array.isArray(groupRes.result) ? groupRes.result : []).map(
|
||||||
},
|
mapConversationToListItem,
|
||||||
{
|
)
|
||||||
id: 2,
|
const merged = [...singles, ...groups].sort((a, b) => {
|
||||||
fromUserName: '廖强',
|
const ta = a.sendTime ? new Date(a.sendTime).getTime() : 0
|
||||||
sendTime: '2018-12-08',
|
const tb = b.sendTime ? new Date(b.sendTime).getTime() : 0
|
||||||
type: 'discussion',
|
return tb - ta
|
||||||
izTop: 1,
|
})
|
||||||
status: 'online',
|
paging.value.complete(merged)
|
||||||
msgFrom: 4012,
|
})
|
||||||
msgTo: 134,
|
.catch(() => {
|
||||||
fromAvatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
|
paging.value.complete(false)
|
||||||
},
|
})
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
fromUserName: '许强',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'group',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'online',
|
|
||||||
msgFrom: 4024,
|
|
||||||
msgTo: 168,
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/f37b1d/fff&text=%E7%8E%8B%E4%BA%94',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
fromUserName: '孙静',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'friend',
|
|
||||||
izTop: 1,
|
|
||||||
status: 'online',
|
|
||||||
msgFrom: 4036,
|
|
||||||
msgTo: 202,
|
|
||||||
fromAvatar: 'https://random.imagecdn.app/100/100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
fromUserName: '白艳',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4048,
|
|
||||||
msgTo: 236,
|
|
||||||
fromAvatar: 'https://picsum.photos/100/100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
fromUserName: '尹芳',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4060,
|
|
||||||
msgTo: 270,
|
|
||||||
fromAvatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
fromUserName: '侯娜',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'group',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4072,
|
|
||||||
msgTo: 304,
|
|
||||||
fromAvatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
fromUserName: '邹涛',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'group',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4084,
|
|
||||||
msgTo: 338,
|
|
||||||
fromAvatar: 'https://dummyimage.com/100x100/59c7b8/fff&text=%E5%85%AD%E5%AD%90',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
fromUserName: '吴洋',
|
|
||||||
sendTime: '2018-12-08',
|
|
||||||
type: 'discussion',
|
|
||||||
izTop: 0,
|
|
||||||
status: 'offline',
|
|
||||||
msgFrom: 4096,
|
|
||||||
msgTo: 372,
|
|
||||||
fromAvatar: 'https://random.imagecdn.app/100/100',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
paging.value.complete(data)
|
|
||||||
nextTick(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
sortByIzTop(dataList.value)
|
|
||||||
dataList.value = [...dataList.value]
|
|
||||||
}, 10)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const sortByIzTop = (arr) => {
|
|
||||||
return arr.sort((a, b) => {
|
|
||||||
if (a.izTop && !b.izTop) {
|
|
||||||
return -1 // a 排在 b 前面
|
|
||||||
} else if (!a.izTop && b.izTop) {
|
|
||||||
return 1 // b 排在 a 前面
|
|
||||||
} else {
|
|
||||||
return 0 // 保持原有顺序
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】会话列表接真实接口-----------
|
||||||
|
|
||||||
const getFirstStr = (val) => {
|
const getFirstStr = (val) => {
|
||||||
return val ? val.substr(0, 1) : val
|
return val ? val.substr(0, 1) : val
|
||||||
}
|
}
|
||||||
const getAvatar = (item) => {
|
const getAvatar = (item) => {
|
||||||
if (['systemNotice'].includes(item.type)) {
|
|
||||||
} else if (['group'].includes(item.type)) {
|
|
||||||
}
|
|
||||||
return getFileAccessHttpUrl(item.fromAvatar) ?? ''
|
return getFileAccessHttpUrl(item.fromAvatar) ?? ''
|
||||||
}
|
}
|
||||||
const getChatType = (item) => {
|
const getChatType = (item) => {
|
||||||
switch (item.type) {
|
return item.lastContent || (item.type === 'group' ? '[群聊]' : '[单聊]')
|
||||||
case 'discussion':
|
|
||||||
return '[聊天消息]'
|
|
||||||
case 'systemNotice':
|
|
||||||
return '[系统消息]'
|
|
||||||
case 'friend':
|
|
||||||
return item.status == 'offline' ? '[离线]' : '[在线]'
|
|
||||||
case 'group':
|
|
||||||
return '[群消息]'
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const handleLongPress = (item) => {
|
|
||||||
bottomOperatePopup.show = true
|
|
||||||
bottomOperatePopup.title = item.fromUserName
|
|
||||||
bottomOperatePopup.data = item
|
|
||||||
bottomOperatePopup.options = options.filter((o) => {
|
|
||||||
if (o.key == 'backtop' && item.izTop == 1) {
|
|
||||||
return false
|
|
||||||
} else if (o.key == 'cancelbacktop' && item.izTop == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const handleChange = ({ option, data }) => {
|
|
||||||
if (['cancelbacktop', 'backtop'].includes(option.key)) {
|
|
||||||
let izTop = 1
|
|
||||||
if (option.key === 'cancelbacktop') {
|
|
||||||
izTop = 0
|
|
||||||
}
|
|
||||||
http
|
|
||||||
.post('/eoa/im/newApi/chatToTop', {
|
|
||||||
id: data.id,
|
|
||||||
izTop,
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.success) {
|
|
||||||
paging.value.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (option.key === 'delete') {
|
|
||||||
http
|
|
||||||
.post('/eoa/im/newApi/removeChat', {
|
|
||||||
id: data.id,
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.success) {
|
|
||||||
paging.value.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 跳转
|
|
||||||
const handleGo = (item) => {
|
const handleGo = (item) => {
|
||||||
if (['systemNotice'].includes(item.type)) {
|
paramsStore.setPageParams('chat', { data: item })
|
||||||
//1.系统消息
|
router.push({ name: 'chat' })
|
||||||
router.push({ name: 'annotationList', params: { backRouteName: 'message' } })
|
|
||||||
} else if (['friend'].includes(item.type)) {
|
|
||||||
//2.聊天
|
|
||||||
paramsStore.setPageParams('chat', { data: item })
|
|
||||||
router.push({ name: 'chat' })
|
|
||||||
} else {
|
|
||||||
///3.群组和讨论组
|
|
||||||
// TODO
|
|
||||||
paramsStore.setPageParams('chat', { data: item })
|
|
||||||
router.push({ name: 'chat' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:2026-07-20 for:【APP对接真实IM】WebSocket 刷新会话列表-----------
|
||||||
const onSocketOpen = () => {
|
const onSocketOpen = () => {
|
||||||
// console.log('启动webSocket')
|
socket.init('websocket')
|
||||||
// socket.init('eoaNewChatSocket')
|
|
||||||
}
|
}
|
||||||
const onSocketReceive = () => {
|
const onSocketReceive = () => {
|
||||||
socket.acceptMessage = function (res) {
|
socket.acceptMessage = function (res) {
|
||||||
if (['event_talk_revoke', 'event_chat_online'].includes(res.event)) {
|
if (res?.cmd !== 'chat') {
|
||||||
const findItem = dataList.value.find((item) => item.id === res.data.id)
|
return
|
||||||
if (findItem) {
|
}
|
||||||
Object.assign(findItem, { ...res.data })
|
const conversationId = res.conversationId
|
||||||
}
|
if (!conversationId) {
|
||||||
} else if (['event_chat_talk'].includes(res.event)) {
|
paging.value?.reload()
|
||||||
const findItem = dataList.value.find((item) => {
|
return
|
||||||
if (item.type == 'friend') {
|
}
|
||||||
return item.msgTo === res.data.msgFrom
|
const findItem = dataList.value.find((item) => item.conversationId === conversationId)
|
||||||
} else if (item.type == 'group') {
|
const preview = res.content || ''
|
||||||
return item.msgTo === res.data.msgTo
|
const sendTime = res.createTime || ''
|
||||||
} else if (item.type == 'discussion') {
|
const myId = userStore.userInfo.userid
|
||||||
return item.msgTo === res.data.msgTo
|
if (findItem) {
|
||||||
|
findItem.lastContent = preview
|
||||||
|
findItem.sendTime = sendTime
|
||||||
|
if (res.senderId !== myId) {
|
||||||
|
findItem.unreadNum = (findItem.unreadNum ?? 0) + 1
|
||||||
|
if (findItem.unreadNum > 99) {
|
||||||
|
findItem.unreadNum = 99
|
||||||
}
|
}
|
||||||
})
|
|
||||||
findItem.unreadNum = findItem.unreadNum ?? 0
|
|
||||||
findItem.unreadNum++
|
|
||||||
if (findItem.unreadNum > 99) {
|
|
||||||
findItem.unreadNum = 99
|
|
||||||
}
|
}
|
||||||
} else if (['event_chat_add_list'].includes(res.event)) {
|
// 移到顶部
|
||||||
// 把当前用户加入组或者聊天
|
const idx = dataList.value.indexOf(findItem)
|
||||||
const findItem = dataList.value.find((item) => item.id === res.data.id)
|
if (idx > 0) {
|
||||||
if (!findItem) {
|
dataList.value.splice(idx, 1)
|
||||||
dataList.value.unshift(res.data)
|
dataList.value.unshift(findItem)
|
||||||
}
|
|
||||||
} else if (['event_chat_delete_list'].includes(res.event)) {
|
|
||||||
// 把当前用户删除群组或者聊天 或者当前用户所在群组或聊天被解散
|
|
||||||
const findIndex = dataList.value.findIndex((item) => (item.msgTo = res.data.msgTo))
|
|
||||||
if (findIndex != -1) {
|
|
||||||
dataList.value.splice(findIndex, 1)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
paging.value?.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:2026-07-20 for:【APP对接真实IM】WebSocket 刷新会话列表-----------
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
onSocketOpen()
|
onSocketOpen()
|
||||||
onSocketReceive()
|
onSocketReceive()
|
||||||
|
paging.value?.reload()
|
||||||
})
|
})
|
||||||
onHide(() => {
|
onHide(() => {})
|
||||||
// socket?.closeSocket()
|
|
||||||
})
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
uni.$on('chatList:unreadClear', (chatItemData) => {
|
uni.$on('chatList:unreadClear', (chatItemData) => {
|
||||||
const findItem = dataList.value.find((item) => item.id === chatItemData.id)
|
const findItem = dataList.value.find(
|
||||||
|
(item) =>
|
||||||
|
item.conversationId === chatItemData?.conversationId || item.id === chatItemData?.id,
|
||||||
|
)
|
||||||
if (findItem) {
|
if (findItem) {
|
||||||
findItem.unreadNum = 0
|
findItem.unreadNum = 0
|
||||||
}
|
}
|
||||||
|
|||||||
137
JeecgUniapp-master/src/service/im/chat.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import { http } from '@/utils/http'
|
||||||
|
|
||||||
|
const BASE = '/sys/im/chat'
|
||||||
|
|
||||||
|
/** IM 会话 VO */
|
||||||
|
export interface ImConversation {
|
||||||
|
conversationId: string
|
||||||
|
convType?: string
|
||||||
|
lastContent?: string
|
||||||
|
lastTime?: string
|
||||||
|
unreadCount?: number
|
||||||
|
targetUserId?: string
|
||||||
|
targetRealname?: string
|
||||||
|
targetUsername?: string
|
||||||
|
targetAvatar?: string
|
||||||
|
groupName?: string
|
||||||
|
memberCount?: number
|
||||||
|
ownerId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IM 消息 VO */
|
||||||
|
export interface ImMessage {
|
||||||
|
id: string
|
||||||
|
conversationId?: string
|
||||||
|
senderId?: string
|
||||||
|
senderName?: string
|
||||||
|
senderAvatar?: string
|
||||||
|
content?: string
|
||||||
|
msgType?: string
|
||||||
|
mine?: boolean
|
||||||
|
createTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IM 联系人 VO */
|
||||||
|
export interface ImContact {
|
||||||
|
id: string
|
||||||
|
username?: string
|
||||||
|
realname?: string
|
||||||
|
avatar?: string
|
||||||
|
orgCodeTxt?: string
|
||||||
|
conversationId?: string
|
||||||
|
lastContent?: string
|
||||||
|
lastTime?: string
|
||||||
|
unreadCount?: number
|
||||||
|
contactType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 会话列表项(适配 APP 列表/聊天页 UI) */
|
||||||
|
export interface ImChatListItem {
|
||||||
|
id: string
|
||||||
|
conversationId: string
|
||||||
|
fromUserName: string
|
||||||
|
fromAvatar: string
|
||||||
|
type: 'friend' | 'group'
|
||||||
|
sendTime: string
|
||||||
|
lastContent: string
|
||||||
|
unreadNum: number
|
||||||
|
msgTo: string
|
||||||
|
targetUserId?: string
|
||||||
|
izTop: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将后端会话 VO 映射为列表 UI 字段 */
|
||||||
|
export function mapConversationToListItem(vo: ImConversation): ImChatListItem {
|
||||||
|
const isGroup = vo.convType === 'group'
|
||||||
|
return {
|
||||||
|
id: vo.conversationId,
|
||||||
|
conversationId: vo.conversationId,
|
||||||
|
fromUserName: isGroup ? vo.groupName || '群聊' : vo.targetRealname || vo.targetUsername || '未知',
|
||||||
|
fromAvatar: isGroup ? '' : vo.targetAvatar || '',
|
||||||
|
type: isGroup ? 'group' : 'friend',
|
||||||
|
sendTime: vo.lastTime || '',
|
||||||
|
lastContent: vo.lastContent || '',
|
||||||
|
unreadNum: vo.unreadCount || 0,
|
||||||
|
msgTo: isGroup ? vo.conversationId : vo.targetUserId || '',
|
||||||
|
targetUserId: vo.targetUserId,
|
||||||
|
izTop: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将消息 VO 映射为气泡字段 */
|
||||||
|
export function mapMessageToBubble(vo: ImMessage) {
|
||||||
|
const sendTime = vo.createTime || ''
|
||||||
|
return {
|
||||||
|
id: vo.id,
|
||||||
|
conversationId: vo.conversationId,
|
||||||
|
msgFrom: vo.senderId,
|
||||||
|
fromUserName: vo.senderName || '',
|
||||||
|
fromAvatar: vo.senderAvatar || '',
|
||||||
|
msgType: vo.msgType || 'text',
|
||||||
|
msgData: vo.content || '',
|
||||||
|
sendTime,
|
||||||
|
mine: !!vo.mine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单聊会话列表 */
|
||||||
|
export const fetchConversations = () => http.get<ImConversation[]>(`${BASE}/conversations`)
|
||||||
|
|
||||||
|
/** 群聊会话列表 */
|
||||||
|
export const fetchGroups = () => http.get<ImConversation[]>(`${BASE}/groups`)
|
||||||
|
|
||||||
|
/** 打开/创建单聊 */
|
||||||
|
export const openConversation = (targetUserId: string) =>
|
||||||
|
http.post<ImConversation>(`${BASE}/open`, {}, { targetUserId })
|
||||||
|
|
||||||
|
/** 消息分页 */
|
||||||
|
export const fetchMessages = (params: {
|
||||||
|
conversationId: string
|
||||||
|
pageNo?: number
|
||||||
|
pageSize?: number
|
||||||
|
beforeTime?: string
|
||||||
|
startTime?: string
|
||||||
|
}) =>
|
||||||
|
http.get<{ records: ImMessage[]; total: number; current: number; size: number }>(`${BASE}/messages`, {
|
||||||
|
pageNo: params.pageNo ?? 1,
|
||||||
|
pageSize: params.pageSize ?? 20,
|
||||||
|
conversationId: params.conversationId,
|
||||||
|
startTime: params.startTime,
|
||||||
|
beforeTime: params.beforeTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 发送消息 */
|
||||||
|
export const sendMessage = (data: { conversationId: string; content: string; msgType?: string }) =>
|
||||||
|
http.post<ImMessage>(`${BASE}/send`, data)
|
||||||
|
|
||||||
|
/** 标记已读 */
|
||||||
|
export const markRead = (conversationId: string) =>
|
||||||
|
http<string>({
|
||||||
|
url: `${BASE}/read`,
|
||||||
|
method: 'PUT',
|
||||||
|
query: { conversationId },
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 本部门成员(联系人) */
|
||||||
|
export const fetchDeptMembers = (keyword?: string) =>
|
||||||
|
http.get<ImContact[]>(`${BASE}/deptMembers`, { keyword })
|
||||||
BIN
JeecgUniapp-master/src/static/im-emoji/1f441-200d-1f5e8.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f479.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f47a.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f47b.png
Normal file
|
After Width: | Height: | Size: 816 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f47d.png
Normal file
|
After Width: | Height: | Size: 809 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f47e.png
Normal file
|
After Width: | Height: | Size: 156 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f47f.png
Normal file
|
After Width: | Height: | Size: 750 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f480.png
Normal file
|
After Width: | Height: | Size: 646 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f48b.png
Normal file
|
After Width: | Height: | Size: 571 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f48c.png
Normal file
|
After Width: | Height: | Size: 763 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f493.png
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f494.png
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f495.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f496.png
Normal file
|
After Width: | Height: | Size: 856 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f497.png
Normal file
|
After Width: | Height: | Size: 998 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f498.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f499.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49a.png
Normal file
|
After Width: | Height: | Size: 499 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49b.png
Normal file
|
After Width: | Height: | Size: 497 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49c.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49d.png
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49e.png
Normal file
|
After Width: | Height: | Size: 889 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f49f.png
Normal file
|
After Width: | Height: | Size: 521 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a2.png
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a4.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a5.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a6.png
Normal file
|
After Width: | Height: | Size: 617 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a8.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4a9.png
Normal file
|
After Width: | Height: | Size: 858 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4ab.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4ac.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4ad.png
Normal file
|
After Width: | Height: | Size: 477 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f4af.png
Normal file
|
After Width: | Height: | Size: 864 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f573.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f5a4.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f5e8.png
Normal file
|
After Width: | Height: | Size: 450 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f5ef.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f600.png
Normal file
|
After Width: | Height: | Size: 806 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f601.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f602.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f603.png
Normal file
|
After Width: | Height: | Size: 825 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f604.png
Normal file
|
After Width: | Height: | Size: 809 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f605.png
Normal file
|
After Width: | Height: | Size: 998 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f606.png
Normal file
|
After Width: | Height: | Size: 913 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f607.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f608.png
Normal file
|
After Width: | Height: | Size: 784 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f609.png
Normal file
|
After Width: | Height: | Size: 760 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60a.png
Normal file
|
After Width: | Height: | Size: 796 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60b.png
Normal file
|
After Width: | Height: | Size: 940 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60c.png
Normal file
|
After Width: | Height: | Size: 723 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60d.png
Normal file
|
After Width: | Height: | Size: 978 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60e.png
Normal file
|
After Width: | Height: | Size: 871 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f60f.png
Normal file
|
After Width: | Height: | Size: 702 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f610.png
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f611.png
Normal file
|
After Width: | Height: | Size: 447 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f612.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f613.png
Normal file
|
After Width: | Height: | Size: 807 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f614.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f615.png
Normal file
|
After Width: | Height: | Size: 592 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f616.png
Normal file
|
After Width: | Height: | Size: 890 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f617.png
Normal file
|
After Width: | Height: | Size: 607 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f618.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f619.png
Normal file
|
After Width: | Height: | Size: 651 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61a.png
Normal file
|
After Width: | Height: | Size: 910 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61b.png
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61c.png
Normal file
|
After Width: | Height: | Size: 1003 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61d.png
Normal file
|
After Width: | Height: | Size: 972 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61e.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f61f.png
Normal file
|
After Width: | Height: | Size: 720 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f620.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f621.png
Normal file
|
After Width: | Height: | Size: 683 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f622.png
Normal file
|
After Width: | Height: | Size: 904 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f623.png
Normal file
|
After Width: | Height: | Size: 854 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f624.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f625.png
Normal file
|
After Width: | Height: | Size: 921 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f626.png
Normal file
|
After Width: | Height: | Size: 597 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f627.png
Normal file
|
After Width: | Height: | Size: 716 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f628.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f629.png
Normal file
|
After Width: | Height: | Size: 956 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62a.png
Normal file
|
After Width: | Height: | Size: 921 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62b.png
Normal file
|
After Width: | Height: | Size: 941 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62c.png
Normal file
|
After Width: | Height: | Size: 640 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62d.png
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62e-200d-1f4a8.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62e.png
Normal file
|
After Width: | Height: | Size: 591 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f62f.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f630.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f631.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f632.png
Normal file
|
After Width: | Height: | Size: 816 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f633.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
JeecgUniapp-master/src/static/im-emoji/1f634.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
JeecgUniapp-master/src/static/im-emoji/1f635-200d-1f4ab.png
Normal file
|
After Width: | Height: | Size: 959 B |