实现IM系统与PC端一致的WebSocket连接,优化聊天功能,支持多端消息同步,更新联系人和会话列表接口,增强用户体验。

This commit is contained in:
geht
2026-07-21 15:53:06 +08:00
parent f24a21a41c
commit 7a9c19e4f3
171 changed files with 661 additions and 747 deletions

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import { randomString } from './uitls'
import md5 from 'md5'
import { useUserStore } from '@/store/user'
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
@@ -8,66 +8,81 @@ class socket {
constructor() {
this.socketUrl = baseUrl
this.socketStart = false
this.socketType = ''
this.socketType = 'websocket'
this._heartbeatTimer = null
this.monitorSocketError()
this.monitorSocketClose()
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?) {
const userStore = useUserStore()
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 {
if (!baseUrl) {
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给服务器发送消息
send(data, callback) {
const userStore = useUserStore()
if (userStore.userInfo.userid) {
if (userStore.userInfo.userid && typeof data === 'object' && data !== null) {
data.userUid = userStore.userInfo.userid
}
console.log(data)
const payload = typeof data === 'string' ? data : JSON.stringify(data)
uni.sendSocketMessage({
data: JSON.stringify(data),
data: payload,
success: () => {
callback && callback(true)
},
@@ -76,61 +91,76 @@ class socket {
},
})
}
acceptMessage(msg) {
console.log(msg)
}
// Socket接收服务器发送过来的消息
socketReceive() {
uni.onSocketMessage((res) => {
console.log('APP:--》收到服务器内容:')
let data = JSON.parse(res.data)
// console.log('收到服务器内容:', data);
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】忽略心跳并解析 JSON-----------
if (res.data === 'ping' || res.data === 'pong') {
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)
})
}
// 关闭Socket
closeSocket() {
this.stopHeartbeat()
uni.closeSocket()
this.socketStart = false
}
// 监听Socket关闭
monitorSocketClose() {
uni.onSocketClose((res) => {
console.log('WebSocket 已关闭!')
this.socketStart = false
this.stopHeartbeat()
setTimeout(() => {
this.init(this.socketType)
}, 3000)
})
}
// 监听Socket错误
monitorSocketError() {
uni.onSocketError(function (res) {
uni.onSocketError((res) => {
this.socketStart = false
console.log('WebSocket连接打开失败请检查')
})
}
// 心跳
getHeartbeat() {
const userStore = useUserStore()
this.send(
{
type: '心跳',
userUid: userStore.userInfo.userid,
},
(val) => {
setTimeout(() => {
if (val) {
// this.getHeartbeat();
} else {
if (!this.socketStart) {
// this.init();
}
}
}, 10000)
},
)
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】心跳 ping-----------
startHeartbeat() {
this.stopHeartbeat()
this._heartbeatTimer = setInterval(() => {
if (!this.socketStart) {
return
}
this.send('ping')
}, 55000)
}
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()
export default mySocket

View File

@@ -4,9 +4,9 @@
style: {
navigationBarTitleText: '聊天',
navigationStyle: 'custom',
disableScroll: true, // 微信禁止页面滚动
disableScroll: true,
'app-plus': {
bounce: 'none', // 禁用 iOS 弹性效果
bounce: 'none',
},
},
}
@@ -23,14 +23,14 @@
>
<view class="wrap">
<!-- 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}">
<view style="transform: scaleY(-1)">
<chat-item :item="item" :playMsgid="playMsgid" @playVoice="handlePlayVoice"></chat-item>
</view>
</template>
<template #bottom>
<chat-input-bar ref="inputBar" @send="doSend" />
<chat-input-bar ref="inputBar" @send="doSend" @image="handleImage" />
</template>
</z-paging>
</view>
@@ -38,354 +38,230 @@
</template>
<script lang="ts" setup>
//
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import { onShow } from '@dcloudio/uni-app'
import { nextTick, onMounted, ref } from 'vue'
import { useUserStore } from '@/store/user'
import { http } from '@/utils/http'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { useToast } from 'wot-design-uni'
import { useRouter } from '@/plugin/uni-mini-router'
import { cache, getFileAccessHttpUrl, hasRoute, formatDate } from '@/common/uitls'
import { TENANT_LIST } from '@/common/constants'
import { formatDate } from '@/common/uitls'
import socket from '@/common/socket'
import { textReplaceEmoji, getEmojiImageUrl } from './emojis'
import { textReplaceEmoji } from './emojis'
import chatInputBar from './components/chat-input-bar.vue'
import chatItem from './components/chat-item.vue'
import { getEnvBaseUrl } from '@/utils/index'
import { useParamsStore } from '@/store/page-params'
import {
fetchMessages,
markRead,
openConversation,
sendMessage,
mapMessageToBubble,
type ImMessage,
} from '@/service/im/chat'
defineOptions({
name: 'chat',
options: {
// apply-shared当前页面样式会影响到子组件样式.(小程序)
// 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 userStore = useUserStore()
const paging = ref(null)
// 信息
const chatObj = ref(null)
// 对方userid 或者 群id 或者 讨论组id
const chatObj = ref<any>(null)
const conversationId = ref('')
const chatto = ref()
const navTitle = ref('')
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 inputBar = ref(null)
const AUDIO = uni.createInnerAudioContext()
const playMsgid = ref('')
let stopWatch: any = null
const paramsStore = useParamsStore()
const backRouteName = ref('message')
const routeMethod = ref('pushTab')
const router = useRouter()
// 是否首次加载
let isFirstLoad = true
let chatItemData = {}
let chatItemData: any = {}
/** 已处理的消息 ID防重复插入 */
const processedMsgIds = new Set<string>()
// 页面初始化
const init = () => {
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】聊天页接真实接口-----------
const init = async () => {
const localData = paramsStore.getPageParams('chat')
const params = localData.data
const params = localData?.data
if (!params) {
return
}
chatObj.value = { ...params }
// 对方头像、群头像、讨论组头像
navTitle.value = params.fromUserName
// 对方userid 或者 群id 或者 讨论组id
chatto.value = chatObj.value.msgTo
chatto.value = params.msgTo || params.targetUserId
chatItemData = params
if (params.type == 'friend') {
// 创建会话数据
creatFriendSession(chatObj.value.msgTo)
conversationId.value = params.conversationId || ''
// 从联系人进入且无会话:先 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()
// 接收消息
onSocketReceive()
// 加载初始页面消息
getMsgList()
doMarkRead()
if (localData.back) {
backRouteName.value = localData.back
routeMethod.value = localData.routeMethod
}
}
// 创建会话数据
const creatFriendSession = (userId) => {
http.post(api.creatFriendSession, {
type: 'friend',
userId,
// 会话就绪后再拉消息,避免与异步 open 竞态
nextTick(() => {
paging.value?.reload()
})
}
const doMarkRead = () => {
if (!conversationId.value) {
return
}
markRead(conversationId.value).then(() => {
uni.$emit('chatList:unreadClear', chatItemData)
}).catch(() => {})
}
const onSocketOpen = () => {
// console.log('启动webSocket')
// socket.init('eoaNewChatSocket')
socket.init('websocket')
}
const onSocketReceive = () => {
var _this = this
socket.acceptMessage = function (res) {
console.log('页面收到的消息=====》', res)
if (res.event == 'event_talk_revoke') {
// 撤回了消息
removeMsg(res)
} else {
// event_chat_talk
if (res.event.startsWith('event_chat')) {
//聊天消息
screenMsg(res)
unreadClear()
}
if (res?.cmd !== 'chat') {
return
}
}
}
const removeMsg = (data) => {
let arr = msgList.value.filter((item) => item.id != data.id)
msgList.value = arr
}
const screenMsg = (msg) => {
// 消息处理
let reuslt = false
if (chatObj.value.type == 'friend') {
reuslt = msg.data.msgFrom == chatto.value && msg.data.msgTo == myuid.value
} else if (['group', 'discussion'].includes(chatObj.value.type)) {
reuslt = msg.data.msgFrom !== myuid.value && msg.data.msgTo == chatto.value
}
if (reuslt) {
console.log('用户消息')
let time = formatDate(msg.data.sendTime, 'yyyy-MM-dd hh:mm:ss')
let id = time.replace(/\:/g, '').replace(/\-/g, '').replace(' ', '')
paging.value?.addChatRecordData(
analysis([
{
fromUserName: msg.data.fromUserName,
msgTo: msg.data.msgTo,
msgFrom: msg.data.msgFrom,
msgData: msg.data.msgData,
fromAvatar: msg.data.fromAvatar,
sendTime: msg.data.sendTime,
msgType: msg.data.msgType,
sendTimeId: id,
fileName: msg.data.fileName,
id: msg.data.id,
},
]),
)
//非自己的消息震动
if (msg.msgFrom != myuid.value) {
console.log('振动')
if (res.conversationId !== conversationId.value) {
return
}
const msgId = res.messageId
//update-begin---author:xsl ---date:2026-07-20 for【IM多端同步】WS 与本地发送竞态去重-----------
if (msgId && processedMsgIds.has(msgId)) {
return
}
if (msgId && dataList.value.some((item: any) => item.id === msgId)) {
processedMsgIds.add(msgId)
setTimeout(() => processedMsgIds.delete(msgId), 5000)
return
}
if (msgId) {
processedMsgIds.add(msgId)
setTimeout(() => processedMsgIds.delete(msgId), 5000)
}
//update-end---author:xsl ---date:2026-07-20 for【IM多端同步】WS 与本地发送竞态去重-----------
const bubble = mapMessageToBubble({
id: res.messageId,
conversationId: res.conversationId,
senderId: res.senderId,
senderName: res.senderName,
senderAvatar: res.senderAvatar,
content: res.content,
msgType: res.msgType || 'text',
createTime: formatWsTime(res.createTime),
})
paging.value?.addChatRecordData(analysis([bubble]))
if (res.senderId !== myuid.value) {
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) => {
let temp = textReplaceEmoji(str)
const temp = textReplaceEmoji(str)
return '<div style="display:inline-block">' + temp + '</div>'
}
const queryList = (pageNo, pageSize) => {
//数据库查询消息列表
let params = {
type: chatObj.value.type,
pageNo: pageNo,
pageSize: pageSize,
msgTo: chatto.value,
// id: myuid.value,
// sort: 'DESC',
if (!conversationId.value) {
paging.value.complete([])
return
}
console.log('params', params)
const data = [
{
id: 1,
fromUserName: '顾平',
sendTime: '1977-11-13',
fromAvatar: 'https://dummyimage.com/100x100/000/fff&text=%E6%9D%8E%E5%9B%9B',
type: 'friend',
izTop: 1,
status: 'offline',
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)
fetchMessages({
conversationId: conversationId.value,
pageNo,
pageSize,
})
.then((res: any) => {
if (!res.success) {
toast.warning(res.message || '加载消息失败')
paging.value.complete(false)
return
}
if (item.msgType == 'voice') {
content = JSON.parse(content)
const records: ImMessage[] = res.result?.records || []
// 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++) {
if (list[i].msgType == 'revoke') {
continue
}
if (list[i].referenceMsgId) {
list[i] = handleReplyMsg(list[i], list)
}
}
}
return data
.catch(() => {
paging.value.complete(false)
})
}
// 加载初始页面消息
const getMsgList = () => {}
const handleReplyMsg = (item, list) => {
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
}
const analysis = (data) => {
if (!data?.length) {
return data || []
}
item.replyContent = replyContent
return item
return data.map((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) => {
if (item.id == playMsgid.value) {
AUDIO.stop()
@@ -398,150 +274,88 @@ const handlePlayVoice = (item) => {
})
}
}
// 播放语音结束
AUDIO.onEnded((res) => {
AUDIO.onEnded(() => {
playMsgid.value = ''
})
// 监听键盘高度改变请不要直接通过uni.onKeyboardHeightChange监听否则可能导致z-paging内置的键盘高度改变监听失效如果不需要切换表情面板则不用写
const keyboardHeightChange = (res) => {
inputBar.value.updateKeyboardHeightChange(res)
inputBar.value?.updateKeyboardHeightChange(res)
}
// 用户尝试隐藏键盘此时如果表情面板在展示中应当通知chatInputBar隐藏表情面板如果不需要切换表情面板则不用写
const hidedKeyboard = () => {
inputBar.value.hidedKeyboard()
inputBar.value?.hidedKeyboard()
}
const doSend = (textMsg) => {
let content = replaceEmoji(textMsg)
//let msg = {'text':content}
//content = (content||'').replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&amp;')
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 (!textMsg?.trim()) {
return
}
let sendData = {
type: 'chatMessage',
data: obj,
if (!conversationId.value) {
toast.warning('会话未就绪')
return
}
console.log('sendData======>', sendData)
let params = {
type: chatObj.value.type,
msgTo: chatObj.value.msgTo,
text: content,
sendMessage({
conversationId: conversationId.value,
content: textMsg.trim(),
msgType: 'text',
}
// http.post(api.sendMsg, params).then((res: any) => {
// console.log('消息发送结果:', res)
// if (!res.success) {
// toast.error(res.message)
// }
// })
})
.then((res: any) => {
if (!res.success || !res.result) {
toast.warning(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) => {
let time = formatDate(new Date().getTime(), 'yyyy-MM-dd hh:mm:ss')
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 handleImage = () => {
toast.warning('图片发送暂未支持')
}
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 = () => {
uni.$emit('chatList:unreadClear', chatItemData)
uni.$emit('chatList:reload')
}
const handleGoChatSetting = () => {
paramsStore.setPageParams('chatSetting', {
type: chatObj.value.type,
chatto: chatto.value,
})
router.push({
name: 'chatSetting',
})
toast.warning('群设置功能后续开放,请从会话列表进入群聊')
}
init()
onShow(() => {
onSocketOpen()
onSocketReceive()
})
onMounted(() => {
uni.$on('chat:updateTile', (title) => {
navTitle.value = title
})
})
onBeforeUnmount(() => {
// socket?.closeSocket()
uni.$off('chat:updateTile')
})
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】聊天页接真实接口-----------
</script>
<style lang="scss" scoped>
//
.wrap {
height: 100%;
}

File diff suppressed because one or more lines are too long

View File

@@ -81,18 +81,18 @@
</template>
<script lang="ts" setup>
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import { nextTick, onMounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { nextTick, ref } from 'vue'
import { useUserStore } from '@/store/user'
import { http } from '@/utils/http'
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 { cache, getFileAccessHttpUrl, hasRoute } from '@/common/uitls'
import { cache, getFileAccessHttpUrl } from '@/common/uitls'
import vPinyin from '../common/vue-py'
import rightConditionFilter from '@/components/RightConditionFilter/RightConditionFilter.vue'
import { TENANT_LIST } from '@/common/constants'
import defaultAvatar from '@/static/default-avatar.png'
import { fetchDeptMembers, openConversation, mapConversationToListItem } from '@/service/im/chat'
const toast = useToast()
const userStore = useUserStore()
@@ -107,23 +107,24 @@ const dataSource = ref([])
const conditionFilter = reactive({ show: false, checked: '', options: [] })
const systemInfo = getApp().globalData.systemInfo
const { height } = systemInfo.safeArea
const queryList = (pageNo, pageSize) => {
const pararms = { pageNo, pageSize, tenantId: conditionFilter.checked }
if (conditionFilter.checked === '') delete pararms.tenantId
http
.get('/sys/user/appQueryUser', pararms)
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】联系人改用 deptMembers-----------
const queryList = () => {
fetchDeptMembers(keyword.value || undefined)
.then((res: any) => {
if (res.success && res.result.length) {
let result = res.result
paging.value.complete(result)
if (res.success && Array.isArray(res.result) && res.result.length) {
paging.value.complete(res.result)
} else if (res.success) {
paging.value.complete([])
} else {
toast.warning(res.message || '加载联系人失败')
paging.value.complete(false)
}
})
.catch((res) => {
.catch(() => {
paging.value.complete(false)
})
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】联系人改用 deptMembers-----------
// 监听接口数据
watch(dataList, () => {
let result = handleResult(dataList.value)
@@ -165,10 +166,26 @@ function handleClear() {
handleSearch()
}
const handleClick = (index: string, data: any) => {
paramsStore.setPageParams('personPage', { backRouteName: 'contacts', data })
router.push({ name: 'personPage' })
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】点击联系人打开单聊-----------
const handleClick = async (_index: string, data: any) => {
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 }) => {
conditionFilter.checked = option.key
paging.value.reload()

View File

@@ -158,17 +158,21 @@ const handleClick = () => {
bottomOperatePopup.show = true
}
}
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】个人页发起单聊带 conversation 上下文-----------
const handleGo = () => {
var parmas = {
const parmas = {
fromAvatar: data.avatar,
fromUserName: data.realname || data.username,
msgFrom: userStore.userInfo.userid,
msgTo: data.id,
targetUserId: data.id,
conversationId: data.conversationId || '',
type: 'friend',
}
paramsStore.setPageParams('chat', { back: 'personPage', routeMethod: 'replace', data: parmas })
router.push({ name: 'chat' })
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】个人页发起单聊带 conversation 上下文-----------
</script>
<style lang="scss" scoped>

View File

@@ -64,6 +64,12 @@ const queryList = () => {
})
}
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 })) {
toast.warning('还未开发~')
return

View File

@@ -7,17 +7,11 @@
:default-page-size="100"
>
<uni-list>
<template v-for="(item, index) in dataList" :key="index">
<template v-for="(item, index) in dataList" :key="item.conversationId || index">
<view
:class="{ list: true, isTop: item.izTop == 1 }"
@longpress.prevent="handleLongPress(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">
<view class="group avatar">
<text class="text-white">{{ getFirstStr(item.fromUserName) }}</text>
@@ -40,306 +34,138 @@
</template>
</uni-list>
</z-paging>
<BottomOperate
v-if="bottomOperatePopup.show"
v-bind="bottomOperatePopup"
@close="() => (bottomOperatePopup.show = false)"
@change="handleChange"
></BottomOperate>
<wd-toast />
</template>
<script setup lang="ts">
import { onLaunch, onShow, onHide, onLoad, onReady } from '@dcloudio/uni-app'
import { http } from '@/utils/http'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { onShow, onHide } from '@dcloudio/uni-app'
import { useToast } from 'wot-design-uni'
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 { cache, getFileAccessHttpUrl, hasRoute } from '@/common/uitls'
import socket from '@/common/socket'
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({
name: 'chatList',
options: {
styleIsolation: 'shared',
},
})
const toast = useToast()
const router = useRouter()
const paramsStore = useParamsStore()
const userStore = useUserStore()
const paging = ref(null)
const avatarList = ref()
const dataList = ref([])
const dataList = ref<ImChatListItem[]>([])
const curPlatform = ref<string>(platform)
// #ifdef APP-IOS
curPlatform.value = 'ios'
// #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 data = [
{
id: 1,
fromUserName: '廖秀兰',
sendTime: '2018-12-08',
type: 'discussion',
izTop: 0,
status: 'offline',
msgFrom: 4000,
msgTo: 100,
fromAvatar: 'https://random.imagecdn.app/100/100',
},
{
id: 2,
fromUserName: '廖强',
sendTime: '2018-12-08',
type: 'discussion',
izTop: 1,
status: 'online',
msgFrom: 4012,
msgTo: 134,
fromAvatar: 'https://q1.qlogo.cn/g?b=qq&nk=190848757&s=100',
},
{
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 // 保持原有顺序
}
})
Promise.all([fetchConversations(), fetchGroups()])
.then(([singleRes, groupRes]: any[]) => {
if (!singleRes?.success && !groupRes?.success) {
toast.warning(singleRes?.message || groupRes?.message || '加载会话失败')
paging.value.complete(false)
return
}
const singles = (singleRes?.success && Array.isArray(singleRes.result) ? singleRes.result : []).map(
mapConversationToListItem,
)
const groups = (groupRes?.success && Array.isArray(groupRes.result) ? groupRes.result : []).map(
mapConversationToListItem,
)
const merged = [...singles, ...groups].sort((a, b) => {
const ta = a.sendTime ? new Date(a.sendTime).getTime() : 0
const tb = b.sendTime ? new Date(b.sendTime).getTime() : 0
return tb - ta
})
paging.value.complete(merged)
})
.catch(() => {
paging.value.complete(false)
})
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】会话列表接真实接口-----------
const getFirstStr = (val) => {
return val ? val.substr(0, 1) : val
}
const getAvatar = (item) => {
if (['systemNotice'].includes(item.type)) {
} else if (['group'].includes(item.type)) {
}
return getFileAccessHttpUrl(item.fromAvatar) ?? ''
}
const getChatType = (item) => {
switch (item.type) {
case 'discussion':
return '[聊天消息]'
case 'systemNotice':
return '[系统消息]'
case 'friend':
return item.status == 'offline' ? '[离线]' : '[在线]'
case 'group':
return '[群消息]'
default:
return ''
}
return item.lastContent || (item.type === 'group' ? '[群聊]' : '[单聊]')
}
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) => {
if (['systemNotice'].includes(item.type)) {
//1.系统消息
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' })
}
paramsStore.setPageParams('chat', { data: item })
router.push({ name: 'chat' })
}
//update-begin---author:xsl ---date:2026-07-20 for【APP对接真实IM】WebSocket 刷新会话列表-----------
const onSocketOpen = () => {
// console.log('启动webSocket')
// socket.init('eoaNewChatSocket')
socket.init('websocket')
}
const onSocketReceive = () => {
socket.acceptMessage = function (res) {
if (['event_talk_revoke', 'event_chat_online'].includes(res.event)) {
const findItem = dataList.value.find((item) => item.id === res.data.id)
if (findItem) {
Object.assign(findItem, { ...res.data })
}
} else if (['event_chat_talk'].includes(res.event)) {
const findItem = dataList.value.find((item) => {
if (item.type == 'friend') {
return item.msgTo === res.data.msgFrom
} else if (item.type == 'group') {
return item.msgTo === res.data.msgTo
} else if (item.type == 'discussion') {
return item.msgTo === res.data.msgTo
if (res?.cmd !== 'chat') {
return
}
const conversationId = res.conversationId
if (!conversationId) {
paging.value?.reload()
return
}
const findItem = dataList.value.find((item) => item.conversationId === conversationId)
const preview = res.content || ''
const sendTime = res.createTime || ''
const myId = userStore.userInfo.userid
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 findItem = dataList.value.find((item) => item.id === res.data.id)
if (!findItem) {
dataList.value.unshift(res.data)
}
} 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)
// 移到顶部
const idx = dataList.value.indexOf(findItem)
if (idx > 0) {
dataList.value.splice(idx, 1)
dataList.value.unshift(findItem)
}
} else {
paging.value?.reload()
}
}
}
//update-end---author:xsl ---date:2026-07-20 for【APP对接真实IM】WebSocket 刷新会话列表-----------
onShow(() => {
onSocketOpen()
onSocketReceive()
paging.value?.reload()
})
onHide(() => {
// socket?.closeSocket()
})
onHide(() => {})
onMounted(() => {
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) {
findItem.unreadNum = 0
}

View 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 })

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Some files were not shown because too many files have changed in this diff Show More