实现IM系统与PC端一致的WebSocket连接,优化聊天功能,支持多端消息同步,更新联系人和会话列表接口,增强用户体验。
@@ -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
|
||||
|
||||
@@ -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, '&')
|
||||
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%;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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 |