浏览代码

聊天通信

zhengnaiwen_citu 10 月之前
父节点
当前提交
b97b66b3e4

+ 281 - 119
src/hooks/web/useIM.js

@@ -1,15 +1,15 @@
 
 
 
-import { ref, onMounted, onUnmounted } from 'vue';
+import { ref, onMounted, onUnmounted, watch } from 'vue';
 import { getConversationSync, getMessageSync, getChatKey } from '@/api/common'
 import { Base64 } from 'js-base64'
 
 import { useUserStore } from '@/store/user'
 import { useLoginType } from '@/store/loginType'
+import { useIMStore } from '@/store/im'
+
 
-const userStore = useUserStore()
-const loginType = useLoginType()
 
 // 配置悟空IM
 import {
@@ -17,11 +17,12 @@ import {
   Channel,
   WKSDK,
   ChannelTypePerson,
-  // ChannelTypeGroup
+  // Conversation,
+  // Message, StreamItem, ChannelTypeGroup, MessageStatus, SyncOptions, MessageExtra, MessageContent
 } from "wukongimjssdk"
 
 const HISTORY_QUERY = {
-  limit: 15,
+  limit: 30,
   startMessageSeq: 0,
   endMessageSeq: 0,
   pullMode: 1
@@ -34,50 +35,152 @@ const ConnectStatus = {
   ConnectFail: 3, // 连接错误
   ConnectKick: 4, // 连接被踢,服务器要求客户端断开(一般是账号在其他地方登录,被踢)
 }
-
-initDataSource()
 // api 接入
-function initDataSource () {
+export function useDataSource () {
+  const userStore = useUserStore() 
+  const loginType = useLoginType()
   // 最近会话数据源
-  WKSDK.shared().config.provider.syncConversationsCallback  = async () => {
-    const query = {
-      msg_count: 100
-    }
-    if (loginType.loginType === 'enterprise') {
-      Object.assign(query, { enterpriseId: userStore.baseInfo.enterpriseId })
+  if (!WKSDK.shared().config.provider.syncConversationsCallback) {
+    WKSDK.shared().config.provider.syncConversationsCallback  = async () => {
+      const query = {
+        msg_count: 100
+      }
+      if (loginType.loginType === 'enterprise') {
+        Object.assign(query, { enterpriseId: userStore.baseInfo.enterpriseId })
+      }
+      const resultConversations = []
+      const resp = await getConversationSync(query)
+      // console.log(resp)
+      const conversationList = resp
+      if (conversationList) {
+        conversationList.forEach(conversation => {
+          conversation.channel = new Channel(conversation.channel_id, conversation.channel_type)
+          conversation.unread = +(conversation.unread || 0)
+          resultConversations.push(conversation)
+        })
+      }
+      return resultConversations
     }
-    const res = await getConversationSync(query)
-    return res
   }
-  // 同步频道消息数据源
-  WKSDK.shared().config.provider.syncMessagesCallback = async function(channel, opts) {
-    // 后端提供的获取频道消息列表的接口数据 然后构建成 Message对象数组返回
-    let resultMessages  = new Array()
-    const query = {
-      channel_id: channel.channelID,
-      channel_type: channel.channelType,
-      start_message_seq: opts.startMessageSeq,
-      end_message_seq: opts.endMessageSeq,
-      limit: opts.limit,
-      pull_mode: opts.pullMode,
-    }
-    if (loginType.loginType === 'enterprise') {
-      Object.assign(query, { enterpriseId: userStore.baseInfo.enterpriseId })
-    }
-    const resp = await getMessageSync(query)
-    const messageList = resp && resp["messages"]
-    if (messageList) {
-        messageList.forEach((msg) => {
-            // const message = Convert.toMessage(msg);
-            resultMessages.push(msg);
-        });
+  if (!WKSDK.shared().config.provider.syncMessagesCallback) {
+    // 同步频道消息数据源
+    WKSDK.shared().config.provider.syncMessagesCallback = async function(channel) {
+      // 后端提供的获取频道消息列表的接口数据 然后构建成 Message对象数组返回
+      let resultMessages  = new Array()
+      const {
+        startMessageSeq: start_message_seq,
+        endMessageSeq: end_message_seq,
+        limit,
+        pullMode: pull_mode
+      } = HISTORY_QUERY
+      const query = {
+        channel_id: channel.channelID,
+        channel_type: channel.channelType,
+        start_message_seq,
+        end_message_seq,
+        limit,
+        pull_mode,
+      }
+      if (loginType.loginType === 'enterprise') {
+        Object.assign(query, { enterpriseId: userStore.baseInfo.enterpriseId })
+      }
+      const resp = await getMessageSync(query)
+      const messageList = resp && resp["messages"]
+      if (messageList) {
+          messageList.forEach((msg) => {
+              // const message = Convert.toMessage(msg);
+              // msg.channel = new Channel(msg.channel_id, msg.channel_type)
+              msg.payload = JSON.parse(Base64.decode(msg.payload))
+              resultMessages.push(msg);
+          });
+      }
+      const more = resp.more === 1
+      return {
+        more,
+        resultMessages
+      }
     }
-    return resultMessages
   }
 }
 
-export const initKey = async () => {
-  // 通过自身userId和企业id获取token和uid
+// function toConversation (conversationMap) {
+//   const conversation = new Conversation()
+//   conversation.channel = new Channel(conversationMap['channel_id'], conversationMap['channel_type'])
+//   conversation.unread = conversationMap['unread'] || 0;
+//   conversation.timestamp = conversationMap['timestamp'] || 0;
+//   // let recents = conversationMap["recents"];
+//   // if (recents && recents.length > 0) {
+//   //     const messageModel = toMessage(recents[0]);
+//   //     conversation.lastMessage = messageModel
+//   // }
+//   conversation.extra = {}
+
+//   return conversation
+// }
+// function toMessage(msgMap) {
+//   const message = new Message();
+//   if (msgMap['message_idstr']) {
+//       message.messageID = msgMap['message_idstr'];
+//   }
+//   // else {
+//   //     message.messageID = new BigNumber(msgMap['message_id']).toString();
+//   // }
+//   if (msgMap["header"]) {
+//       message.header.reddot = msgMap["header"]["red_dot"] === 1 ? true : false
+//   }
+//   // if (msgMap["setting"]) {
+//   //     message.setting = Setting.fromUint8(msgMap["setting"])
+//   // }
+//   if (msgMap["revoke"]) {
+//       message.remoteExtra.revoke = msgMap["revoke"] === 1 ? true : false
+//   }
+//   if(msgMap["message_extra"]) {
+//       const messageExtra = msgMap["message_extra"]
+//       message.remoteExtra = this.toMessageExtra(messageExtra)
+//   }
+  
+//   message.clientSeq = msgMap["client_seq"]
+//   message.channel = new Channel(msgMap['channel_id'], msgMap['channel_type']);
+//   message.messageSeq = msgMap["message_seq"]
+//   message.clientMsgNo = msgMap["client_msg_no"]
+//   message.streamNo = msgMap["stream_no"]
+//   message.streamFlag = msgMap["stream_flag"]
+//   message.fromUID = msgMap["from_uid"]
+//   message.timestamp = msgMap["timestamp"]
+//   message.status = MessageStatus.Normal
+ 
+//   const contentObj = JSON.parse(Base64.decode(msgMap["payload"]))
+//   // const contentObj = JSON.parse(decodedBuffer.toString('utf8'))
+//   let contentType = 0
+//   if (contentObj) {
+//       contentType = contentObj.type
+//   }
+//   const messageContent = WKSDK.shared().getMessageContent(contentType)
+//   if (contentObj) {
+//       messageContent.decode(this.stringToUint8Array(JSON.stringify(contentObj)))
+//   }
+//   message.content = messageContent
+
+//   message.isDeleted = msgMap["is_deleted"] === 1
+
+//   const streamMaps = msgMap["streams"]
+//   if(streamMaps && streamMaps.length>0) {
+//       const streams = []
+//       for (const streamMap of streamMaps) {
+//           const streamItem = new StreamItem()
+//           streamItem.clientMsgNo = streamMap["client_msg_no"]
+//           streamItem.streamSeq = streamMap["stream_seq"]
+//           streams.push(streamItem)
+//       }
+//       message.streams = streams
+//   }
+
+//   return message
+// }
+
+async function getKey () {
+  const userStore = useUserStore()
+  const loginType = useLoginType()
   const keyQuery = {
     userId: userStore.userInfo.id
   }
@@ -85,129 +188,188 @@ export const initKey = async () => {
     Object.assign(keyQuery, { enterpriseId: userStore.baseInfo.enterpriseId })
   }
   const { uid, wsUrl, token } = await getChatKey(keyQuery)
+  return {
+    uid, wsUrl, token
+  }
+}
+
+export const useIM = async () => {
+  const IM = useIMStore()
+  const unreadCount = ref(0)
+  const connected = ref(0)
+  // 通过自身userId和企业id获取token和uid
+  const { uid, wsUrl, token } = await getKey()
+  IM.setUid(uid)
   // 单机模式可以直接设置地址
   WKSDK.shared().config.addr = 'ws://' + wsUrl // 默认端口为5200
   // 认证信息
   WKSDK.shared().config.uid = uid // 用户uid(需要在悟空通讯端注册过)
   WKSDK.shared().config.token = token // 用户token (需要在悟空通讯端注册过)
-  return {
-    uid
-  }
-}
-
-export function initConnect (userId, enterpriseId, callback) {
   
-  const connected = ref(false)
-  const conversationList = ref([])
-  const messageItems = ref([])
-  const toUid = ref()
-  const channel = ref()
-  // 连接
-  console.log(WKSDK.shared().config)
-  onMounted(() => {
+  // onMounted(() => {
+    // console.log('1')
     // 连接状态监听
     WKSDK.shared().connectManager.addConnectStatusListener(connectStatusListener)
-    // 消息发送状态监听
-    WKSDK.shared().chatManager.addMessageStatusListener(statusListen)
+    // console.log('2')
     // 常规消息监听
     WKSDK.shared().chatManager.addMessageListener(messageListen)
     // 连接
+    // console.log('连接')
     WKSDK.shared().connectManager.connect()
-  })
+  // })
   onUnmounted(() => {
     WKSDK.shared().connectManager.removeConnectStatusListener(connectStatusListener)
-    // 消息发送状态监听移除
-    WKSDK.shared().chatManager.removeMessageStatusListener(statusListen)
     // 常规消息监听移除
     WKSDK.shared().chatManager.removeMessageListener(messageListen)
     // 连接状态监听移除
     WKSDK.shared().connectManager.disconnect()
   })
 
-  const connectStatusListener = async (status) => {
-    console.log('连接状态', status)
-    if (status === ConnectStatus.Connected) {
-      console.log('连接成功')
-      connected.value = true
-      // 连接成功 创建channel
-      // const { message } = await syncConversation()
-      // conversationList.value = message
-      if (!userId) {
-        return
-      }
-      await initChart(userId, enterpriseId)
-      
-      callback()
-    } else {
-      connected.value = false
-      conversationList.value = []
-      toUid.value = null
-    }
+  async function messageListen (message) {
+    console.log('收到消息', message)
+    IM.setFromChannel(message.channel.channelID)
+    const count = WKSDK.shared().conversationManager.getAllUnreadCount()
+    IM.setNewMsg(count)
+    unreadCount.value = count
+    // console.log('未读消息数', count)
+    // 创建通道
+  }
+  async function connectStatusListener (status) {
+    // console.log('连接状态', status === ConnectStatus.Connected)
+    connected.value = status === ConnectStatus.Connected
   }
 
+  return {
+    unreadCount,
+    connected
+  }
+}
+
+export function initConnect (callback = () => {}) {
+  const IM = useIMStore()
+  const conversationList = ref([])
+  const messageItems = ref([])
+
+  watch(
+    () => IM.newMsg,
+    () => {
+      // 未读消息变化
+      syncConversation()
+      // 拉取最新消息 查看是否是自己的数据
+
+    },
+    {
+      deep: true,
+      immediate: true
+    }
+  )
+  onMounted(async () => {
+    // 消息发送状态监听
+    WKSDK.shared().chatManager.addMessageStatusListener(statusListen)
+    // 常规消息监听
+    // WKSDK.shared().chatManager.addMessageListener(messageListen)
+  })
+  onUnmounted(() => {
+    // 消息发送状态监听移除
+    WKSDK.shared().chatManager.removeMessageStatusListener(statusListen)
+    // 常规消息监听移除
+    // WKSDK.shared().chatManager.removeMessageListener(messageListen)
+  })
+
   // 消息发送状态监听
   function statusListen (packet) {
     if (packet.reasonCode === 1) {
       // 发送成功
       console.log('发送成功')
+      // 添加一组成功数据
+      callback()
     } else {
       // 发送失败
+      console.log('发送失败')
+      // 添加一组失败数据
+      // callback()
     }
   }
 
   // 常规消息监听
-  async function messageListen (message) {
-    console.log('收到消息', message)
-    const { channel: _channel } = message
-    const conversation = WKSDK.shared().conversationManager.findConversation(_channel)
-    if(!conversation) {
-      // 如果最近会话不存在,则创建一个空的会话
-      WKSDK.shared().conversationManager.createEmptyConversation(_channel)
-    }
-    channel.value = _channel
-    getMessage(_channel)
-    // 创建通道
-  }
-
-  async function getMessage (channel) {
-    const res = await WKSDK.shared().chatManager.syncMessages(channel, HISTORY_QUERY)
-    // console.log(res)
-    messageItems.value = res.map(_e => {
-      _e.payload = JSON.parse(Base64.decode(_e.payload))
-      return _e
-    })
+  // async function messageListen (message) {
+  //   console.log('收到消息', message)
+  //   syncConversation()
+  // }
+  // 同步最近会话
+  async function syncConversation () {
+    const res = await WKSDK.shared().conversationManager.sync()
+    conversationList.value = res
+    return res
+    // return new Promise((resolve, reject) => {
+    //   WKSDK.shared().conversationManager.sync().then(res => {
+    //     conversationList.value = res
+    //     resolve(res)
+    //   }).catch(error => {
+    //     reject(error)
+    //   })
+    // })
   }
 
-  // 发起聊天
-  async function initChart (userId, enterpriseId) {
-    const { uid } = await getChatKey({userId, enterpriseId})
-    const _channel = new Channel(uid, ChannelTypePerson)
-    channel.value = _channel
+  return {
+    // connected,
+    conversationList,
+    messageItems,
+    // channel
   }
+}
 
-  // 同步最近会话
-  // async function syncConversation () {
-  //   return new Promise((resolve, reject) => {
-  //     WKSDK.shared().conversationManager.sync().then(res => {
-  //       resolve(res)
-  //     }).catch(error => {
-  //       reject(error)
-  //     })
-  //   })
-  // }
+// export async function getRecentMessages (channel) {
+//   const { resultMessages, more } = await WKSDK.shared().chatManager.syncMessages(channel, HISTORY_QUERY)
+//   // console.log(res)
+//   // return resultMessages.map(_e => {
+//   //   _e.payload = JSON.parse(Base64.decode(_e.payload))
+//   //   return _e
+//   // })
+// }
 
+// 发起聊天
+export async function initChart (userId, enterpriseId) {
+  const channel = ref()
+  // const list = ref([])
+  const query = {
+    userId,
+    enterpriseId
+  }
+  // 创建聊天频道
+  const { uid } = await getChatKey(query)
+  const _channel = new Channel(uid, ChannelTypePerson)
+  channel.value = _channel
+  const conversation = WKSDK.shared().conversationManager.findConversation(_channel)
+  if(!conversation) {
+    // 如果最近会话不存在,则创建一个空的会话
+    WKSDK.shared().conversationManager.createEmptyConversation(_channel)
+  }
+  const res = await getMoreMessages(1, _channel)
   return {
-    connected,
-    conversationList,
-    messageItems,
-    channel
+    channel,
+    ...res
   }
 }
 
+// 翻页
+export async function getMoreMessages (pageSize, channel) {
+  const list = ref([])
+  Object.assign(HISTORY_QUERY, {
+    startMessageSeq: (pageSize - 1) * HISTORY_QUERY.limit
+  })
+  const { resultMessages, more } = await WKSDK.shared().chatManager.syncMessages(channel)
+  list.value = resultMessages
+  return {
+    list,
+    more
+  }
+}
 
 export function send (text, _channel) {
+  
   const _text = new MessageText(text) // 文本消息
-  console.log('发送', _text)
+  console.log('发送', _text, _channel)
   WKSDK.shared().chatManager.send(_text, _channel)
 }
 

+ 39 - 0
src/layout/company/message.vue

@@ -0,0 +1,39 @@
+<template>
+  <v-badge
+    color="error"
+    :content="unreadCount"
+    :model-value="unreadCount > 0"
+  >
+    <v-btn class="ml-1" size="small" icon="mdi-bell-outline" @click="router.push({ path: '/recruit/enterprise/communication' })"></v-btn>
+  </v-badge>
+</template>
+
+<script setup>
+defineOptions({ name: 'personal-message' })
+import { watch } from 'vue'
+import { useIM, useDataSource } from '@/hooks/web/useIM'
+import { useIMStore } from '@/store/im'
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+const _im = useIMStore()
+useDataSource()
+
+const {
+  unreadCount,
+  connected
+} = await useIM()
+
+watch(
+  () => connected.value,
+  (val) => {
+    console.log('connected变化', val)
+    _im.setConnected(val)
+
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 8 - 6
src/layout/company/navBar.vue

@@ -74,7 +74,8 @@
             </v-list>
           </v-menu> -->
 
-          <v-btn size="small" icon="mdi-bell-outline" @click="router.push('/recruit/enterprise/communication')"></v-btn>
+          <!-- <v-btn size="small" icon="mdi-bell-outline" @click="router.push('/recruit/enterprise/communication')"></v-btn> -->
+          <MessageNotification></MessageNotification>
         </div>
       </div>
     </v-toolbar>
@@ -94,9 +95,10 @@ import {
 import { computed, ref } from 'vue'
 import { getToken } from '@/utils/auth'
 import { useUserStore } from '@/store/user'; const userStore = useUserStore()
-import { useLocaleStore } from '@/store/locale'; const localeStore = useLocaleStore()
+// import { useLocaleStore } from '@/store/locale'; const localeStore = useLocaleStore()
 import { useRouter } from 'vue-router'; const router = useRouter()
 import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
+import MessageNotification from './message.vue'
 defineOptions({ name: 'personal-navbar' })
 
 defineProps({
@@ -143,10 +145,10 @@ userStore.$subscribe((mutation, state) => {
 })
 
 // 语言切换
-const handleChangeLocale = (item) => {
-  localeStore.setCurrentLocale(item)
-  location.reload()
-}
+// const handleChangeLocale = (item) => {
+//   localeStore.setCurrentLocale(item)
+//   location.reload()
+// }
 
 // 企业相关操作
 const show = ref(false)

+ 39 - 0
src/layout/personal/message.vue

@@ -0,0 +1,39 @@
+<template>
+  <v-badge
+    color="error"
+    :content="unreadCount"
+    :model-value="unreadCount > 0"
+  >
+    <v-btn class="ml-1" size="small" icon="mdi-bell-outline" @click="router.push({ path: '/recruit/personal/message' })"></v-btn>
+  </v-badge>
+</template>
+
+<script setup>
+defineOptions({ name: 'personal-message' })
+import { watch } from 'vue'
+import { useIM, useDataSource } from '@/hooks/web/useIM'
+import { useIMStore } from '@/store/im'
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+const _im = useIMStore()
+useDataSource()
+
+const {
+  unreadCount,
+  connected
+} = await useIM()
+
+watch(
+  () => connected.value,
+  (val) => {
+    console.log('connected变化', val)
+    _im.setConnected(val)
+
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 19 - 7
src/layout/personal/navBar.vue

@@ -86,7 +86,15 @@
           <div class="d-flex align-center" v-if="getToken()">
             <!-- <span class="cursor-pointer mx-5" @click="router.push({ path: '/recruit/personal/TaskCenter' })">{{ $t('sys.signIn') }}</span> -->
             <!-- <span class="cursor-pointer" @click="router.push({ path: '/recruit/personal/message' })">{{ $t('sys.news') }}</span> -->
-            <v-btn class="ml-1" size="small" icon="mdi-bell-outline" @click="router.push({ path: '/recruit/personal/message' })"></v-btn>
+            <!-- <v-badge
+              color="error"
+              dot
+              :model-value="useIM.unreadCount > 0"
+            >
+              <v-btn class="ml-1" size="small" icon="mdi-bell-outline" @click="router.push({ path: '/recruit/personal/message' })"></v-btn>
+            </v-badge> -->
+            <message-notification v-if="getToken()"></message-notification>
+            
           </div>
         </div>
         
@@ -105,11 +113,13 @@
 import { ref } from 'vue'
 import { getToken } from '@/utils/auth'
 import { useUserStore } from '@/store/user'
-import { useLocaleStore } from '@/store/locale'
+// import { useLocaleStore } from '@/store/locale'
 import { useI18n } from '@/hooks/web/useI18n'
 import CtDialog from '@/components/CtDialog'
 import { useRouter } from 'vue-router'; const router = useRouter()
 import { getUserBindEnterpriseList, getUserRegisterEnterpriseApply } from '@/api/personal/user'
+import MessageNotification from './message.vue'
+// import { useIMStore } from '@/store/im'
 defineOptions({ name: 'personal-navbar' })
 
 defineProps({
@@ -119,8 +129,10 @@ defineProps({
   }
 })
 
+// const useIM = useIMStore()
+
 const { t } = useI18n()
-const localeStore = useLocaleStore()
+// const localeStore = useLocaleStore()
 const userStore = useUserStore()
 
 const list = ref([
@@ -200,10 +212,10 @@ const handleLogin = () => {
 }
 
 // 语言切换
-const handleChangeLocale = (item) => {
-  localeStore.setCurrentLocale(item)
-  location.reload()
-}
+// const handleChangeLocale = (item) => {
+//   localeStore.setCurrentLocale(item)
+//   location.reload()
+// }
 </script>
 
 <style lang="scss" scoped>

+ 34 - 0
src/store/im.js

@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useIMStore = defineStore('IM', 
+  () => {
+    const uid = ref('')
+    const connected = ref(false)
+
+    const newMsg = ref(0)
+    const fromChannel = ref('')
+
+    const setConnected = (val) => {
+      connected.value = val
+    }
+    const setUid = (val) => {
+      uid.value = val
+    }
+    const setNewMsg = (val) => {
+      newMsg.value = val
+    }
+    const setFromChannel = (val) => {
+      fromChannel.value = val
+    }
+    return {
+      connected,
+      uid,
+      newMsg,
+      fromChannel,
+      setFromChannel,
+      setNewMsg,
+      setUid,
+      setConnected
+    }
+})

+ 6 - 17
src/store/user.js

@@ -17,8 +17,9 @@ import Snackbar from '@/plugins/snackbar'
 import { timesTampChange } from '@/utils/date'
 import { updateEventList } from '@/utils/eventList'
 import { getBaseInfoDictOfName } from '@/utils/getText'
-import { getChatKey } from '@/api/common'
+// import { useIMStore } from './im'
 
+// const useIM = useIMStore()
 
 
 export const useUserStore = defineStore('user',
@@ -29,8 +30,7 @@ export const useUserStore = defineStore('user',
       userInfo: localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : {}, // 当前登录账号信息
       baseInfo: localStorage.getItem('baseInfo') ? JSON.parse(localStorage.getItem('baseInfo')) : {}, // 人才信息
       userAccount: {}, // 用户账户信息
-      enterpriseUserAccount: {}, // 企业账户信息
-      IMConfig: localStorage.getItem('IMConfig') ? JSON.parse(localStorage.getItem('IMConfig')) : {} // IM聊天密钥
+      enterpriseUserAccount: {} // 企业账户信息
     }),
     actions: {
       // 短信登录
@@ -51,19 +51,7 @@ export const useUserStore = defineStore('user',
           }).catch(err => { reject(err) })
         })
       },
-      // 获取聊天密钥
-      getChatKey () {
-        return new Promise((resolve, reject) => {
-          getChatKey({ userId: this.userInfo.id })
-          .then(res => {
-            this.IMConfig = res
-            localStorage.setItem('IMConfig', JSON.stringify(res))
-            resolve()
-          }).catch(error => {
-            reject(error)
-          })
-        })
-      },
+
       // 密码登录
       async handlePasswordLogin(data) {
         return new Promise((resolve, reject) => {
@@ -89,7 +77,8 @@ export const useUserStore = defineStore('user',
           this.userInfo = data
           localStorage.setItem('userInfo', JSON.stringify(data))
           // 获取密钥
-          this.getChatKey()
+          // this.getChatKey()
+          // useIM.init()
           updateEventList(true) // 获取规则配置跟踪列表
           this.getUserAccountInfo()
         } catch (error) {

+ 75 - 22
src/views/recruit/components/message/components/chatting.vue

@@ -1,23 +1,36 @@
 <template>
   <div class="chatting d-flex flex-column">
+    <v-overlay
+      :model-value="overlay"
+      contained
+      class="align-center justify-center"
+    >
+      <v-progress-circular
+        color="primary"
+        size="64"
+        indeterminate
+      ></v-progress-circular>
+    </v-overlay>
     <div class="top-info">
       <div class="user-info d-flex align-center">
         <p class="d-flex align-center float-left">
-          <span class="name">{{ info.contact?.name }}</span>
-          <span>{{ info.address }}</span>
-          <span class="septal-line"></span>
-          <span>{{ info.contact?.postNameCn }}</span>
+          <span class="name">{{ info.name }}</span>
+          <template v-if="info.enterpriseId">
+            <span>{{ info.postNameCn }}</span>
+            <span class="septal-line"></span>
+            <span>{{ info.enterpriseName }}</span>
+          </template>
         </p>
       </div>
-      <div class="position-content" v-if="!isEnterprise">
+      <!-- <div class="position-content" v-if="!isEnterprise">
         <div class="d-flex mb-2">
           <div class="font-weight-black">{{ info.name }}</div>
           <div class="position-content-emolument">{{ info.payFrom }} - {{ info.payFrom }}</div>
         </div>
-        <div class="text-subtitle-2">{{ info.enterprise?.name }}</div>
+        <div class="text-subtitle-2">{{ info?.enterprise?.name }}</div>
         <div class="pt-3">
           <v-chip
-            v-for="(item, index) in info.enterprise?.welfareList"
+            v-for="(item, index) in info?.enterprise?.welfareList"
             :key="item + index"
             color="green"
             label
@@ -26,15 +39,27 @@
             {{ item }}
           </v-chip>
         </div>
-        <!-- <div>{{ info.content }}</div> -->
-      </div>
+      </div> -->
     </div>
-    <div class="mt-3 message-box" @scroll="handleScroll" ref="chatRef">
+    <v-divider></v-divider>
+    <div class="my-3 message-box" @scroll="handleScroll" ref="chatRef">
       <div>
+        <div class="d-flex justify-center" v-if="hasMore">
+          <v-btn :loading="loading" variant="text" color="primary" size="small" @click="handleMore">查看更多</v-btn>
+        </div>
         <div v-for="(val, i) in items" :key="i" :id="val.id">
           <div class="time-box">{{ timesTampChange(+(val.timestamp.padEnd(13, '0'))) }}</div>
-          <div :class="['message-view_item', val.from_uid === uid ? 'is-self' : 'is-other']">
-            <div style="width: 40px; height: 40px;"><v-img :src="val.avatar" :width="40" height="40" rounded></v-img></div>
+          <div :class="['message-view_item', val.from_uid === IM.uid ? 'is-self' : 'is-other']">
+            <div style="width: 40px; height: 40px;">
+              <v-avatar>
+                <v-img
+                  :src="val.from_uid === IM.uid ? mAvatar : info.avatar"
+                  :width="40"
+                  height="40"
+                  rounded
+                ></v-img>
+              </v-avatar>
+            </div>
             <div class="message-text">{{ val.payload.content }}</div>
           </div>
         </div>
@@ -51,6 +76,7 @@
           no-resize
           bg-color="white"
           variant="plain"
+          :disabled="Object.keys(info).length === 0"
         >
           <!-- @keydown.stop.prevent="handleKeyDown" -->
           <template #append-inner>
@@ -69,12 +95,15 @@
 
 <script setup>
 defineOptions({ name: 'message-chatting'})
-import { ref, nextTick, onMounted, watch, defineEmits, inject } from 'vue'
+import { ref, nextTick, onMounted, defineEmits } from 'vue'
 import { timesTampChange } from '@/utils/date'
-const isEnterprise = inject('isEnterprise')
+import { useIMStore } from '@/store/im'
+
+import { useUserStore } from '@/store/user'
+// const isEnterprise = inject('isEnterprise')
 
 const emits = defineEmits(['inputVal'])
-const props = defineProps({
+defineProps({
   items: {
     type: Array,
     default: () => []
@@ -86,15 +115,19 @@ const props = defineProps({
   uid: {
     type: String,
     default: ''
+  },
+  hasMore: {
+    type: Boolean,
+    default: false
   }
 })
 
-watch(
-  () => props.items,
-  (res) => {
-    console.log('列表', res)
-  }
-)
+const overlay = ref(false)
+const IM = useIMStore()
+const userStore = useUserStore()
+const loading = ref(false)
+
+const mAvatar = userStore.baseInfo?.avatar || 'https://minio.citupro.com/dev/menduner/7.png'
 
 const chatRef = ref()
 const inputVal = ref()
@@ -117,6 +150,9 @@ onMounted(() => {
   })
 })
 
+const handleMore = () => {
+  emits('handleMore')
+}
 
 const handleSend = () => {
   emits('handleSend', inputVal)
@@ -171,12 +207,29 @@ const handleScroll = (e) => {
     // })
   }
 }
+
+const changeOverlay = (val) => {
+  overlay.value = val
+}
+
+const changeLoading = (val) => {
+  loading.value = val
+}
 // const handleKeyDown = (event) => {
 //   if (event.keyCode === 13) {
     
 //     console.log(event)
 //   } 
 // }
+const reset = () => {
+  inputVal.value = ''
+}
+defineExpose({
+  reset,
+  changeLoading,
+  scrollBottom,
+  changeOverlay
+})
 </script>
 
 <style scoped lang="scss">
@@ -285,7 +338,7 @@ input {
 /* 滚动条样式 */
 ::-webkit-scrollbar {
   -webkit-appearance: none;
-  width: 0px;
+  width: 10px;
   height: 0px;
 }
 /* 滚动条内的轨道 */

+ 102 - 48
src/views/recruit/components/message/index.vue

@@ -7,7 +7,7 @@
       </div>
       <div class="message-chat-box mt-5">
         <v-overlay
-          :model-value="!connected"
+          :model-value="!IM.connected"
           contained
           class="align-center justify-center"
         >
@@ -18,17 +18,22 @@
           ></v-progress-circular>
         </v-overlay>
         <div v-if="conversationList.length">
-          <v-list density="compact">
+          <v-list density="compact" mandatory @update:selected="handleChange">
             <v-list-item
               v-for="(val, i) in conversationList"
               :key="i"
               :value="val"
               color="primary"
-              :title="val.channel_id"
-              :subtitle="val.timestamp"
+              class="mb-2"
+              :active="val.userInfoVo && val.userInfoVo.userInfoResp.userId === info.userId"
+              :title="val.userInfoVo ? val.userInfoVo.userInfoResp.name : '系统消息'"
+              :subtitle="timesTampChange(+val.timestamp.padEnd(13, '0'))"
             >
+              <template v-slot:subtitle="{ subtitle }">
+                <div class="mt-2">{{ subtitle }}</div>
+              </template>
               <template v-slot:prepend>
-                <v-avatar :image="val.avatar || 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
+                <v-avatar :image="val.userInfoVo ? val.userInfoVo.userInfoResp.avatar : 'https://minio.citupro.com/dev/menduner/7.png'"></v-avatar>
               </template>
               <template v-slot:append>
                 <v-badge
@@ -52,39 +57,81 @@
       <div v-if="showRightNoData" class="right-noData">
         <Empty :elevation="false" message="与您进行过沟通的 Boss 都会在左侧列表中显示"></Empty>
       </div>
-      <Chatting :items="messageItems" :info="info" :uid="uid" @handleSend="handleUpdate"></Chatting>
+      <Chatting ref="chatRef" :items="messageItems" :info="info" :has-more="hasMore" @handleSend="handleUpdate" @handleMore="handleGetMore"></Chatting>
     </div>
   </div>
 </template>
 
 <script setup>
 defineOptions({ name: 'personal-message-index'})
-import { ref, inject } from 'vue'
+import { timesTampChange } from '@/utils/date'
+import { ref, inject, watch, nextTick, onMounted } from 'vue'
 import Chatting from './components/chatting.vue'
-import { initConnect, send, initKey } from '@/hooks/web/useIM'
+import { initConnect, send, initChart, getMoreMessages } from '@/hooks/web/useIM'
 import { useRoute } from 'vue-router'
 import { getPositionDetails } from '@/api/position'
-// import { useUserStore } from '@/store/user'
+import { getUserInfo } from '@/api/personal/user'
+import { useIMStore } from '@/store/im'
+
+const chatRef = ref()
+
+const IM = useIMStore()
 
-// const userStore = useUserStore()
 const isEnterprise = inject('isEnterprise')
 // 实例
 const route = useRoute()
 
-// const channel = ref(null)
-
-// const enterpriseId = isEnterprise ? userStore.baseInfo.enterpriseId : route.query.enterprise
+if (route.query.id) {
+  const api = route.query.enterprise ? getPositionDetails : getUserInfo
+  const res = await api({ id: route.query.id })
+  console.log(res)
+  onMounted(() => {
+    nextTick(() => {
+      const items = [
+        {
+          userInfoVo: {
+            userInfoResp: res.contact
+          }
+        }
+      ]
+      handleChange(items)
+    })
+  })
+}
 
-const { uid } = await initKey()
-const {
-  connected,
-  conversationList,
-  messageItems,
-  channel
-} = initConnect(route.query.id, route.query.enterprise)
+const channelItem = ref(null)
+const messageItems = ref([])
+const pageSize = ref(1)
+const hasMore = ref(false)
 
+if (!IM) {
+  console.log('IM is disconnected')
+}
 
+const { conversationList } = initConnect(async () => {
+  // 发送成功
+  const { list } = await getMoreMessages(1, channelItem.value)
+  messageItems.value = list.value
+  chatRef.value.scrollBottom()
+})
 
+watch(
+  () => conversationList.value,
+  async () => {
+    // console.log('发生变化', val)
+    // 数据发生变化
+    if (channelItem.value && IM.fromChannel === channelItem.value.channelID) {
+      // 更新
+      const { list } = await getMoreMessages(1, channelItem.value)
+      messageItems.value = list.value
+      chatRef.value.scrollBottom()
+    }
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
 
 const showRightNoData = ref(false)
 const searchInputVal = ref()
@@ -100,40 +147,47 @@ const textItem = ref({
 
 const info = ref({})
 
-if (route.query.job) {
-  const res = await getPositionDetails({ id: route.query.job })
-  info.value = res
-}
-
-// 左侧聊天列表
-// const chatList = ref([
-//   {
-//     id: 1,
-//     name: '钟女士',
-//     avatar: 'https://minio.citupro.com/dev/menduner/tx1.jpg',
-//     enterpriseName: '泰康泰康泰康泰康泰康泰康',
-//     postName: '人事HR',
-//     updateTime: 1715337950000,
-//     tip: '你好'
-//   },
-//   {
-//     id: 2,
-//     name: '林先生',
-//     avatar: 'https://minio.citupro.com/dev/menduner/tx1.jpg',
-//     enterpriseName: '众宝联合',
-//     postName: '人力资源主管',
-//     updateTime: 1715337950000,
-//     tip: '你好'
-//   }
-// ])
-
 const handleUpdate = (val) => {
-  send(val.value, channel.value)
+  send(val.value, channelItem.value)
+  chatRef.value.reset()
 }
 
 const handleSearch = () => {
   console.log(searchInputVal.value, 'search')
 }
+
+async function handleChange (items) {
+  try {
+    chatRef.value.changeOverlay(true)
+    const { userInfoVo } = items.pop()
+    info.value = userInfoVo?.userInfoResp ?? { name: '系统消息' }
+    const userId = userInfoVo.userInfoResp.userId
+    const enterpriseId = userInfoVo.userInfoResp.enterpriseId || undefined
+    const { channel, list, more } = await initChart(userId, enterpriseId)
+    channelItem.value = channel.value
+    messageItems.value = list.value
+    hasMore.value = more
+    chatRef.value.scrollBottom()
+  } catch (error) {
+    messageItems.value = []
+  } finally {
+    chatRef.value.changeOverlay(false)
+  }
+}
+
+const handleGetMore = async () => {
+  try {
+    chatRef.value.changeLoading(true)
+    pageSize.value++
+    const { list, more } = await getMoreMessages(pageSize.value, channelItem.value)
+    messageItems.value.unshift(...list.value)
+    hasMore.value = more
+    // chatRef.value.scrollBottom()
+  } finally {
+    chatRef.value.changeLoading(false)
+  }
+}
+
 </script>
 
 <style scoped lang="scss">

+ 2 - 1
src/views/recruit/personal/position/components/details.vue

@@ -180,6 +180,7 @@ const info = ref({})
 const positionInfo = ref({})
 const getPositionDetail = async () => {
   const data = await getPositionDetails({ id })
+  console.log('daaa', data)
   info.value = data
   positionInfo.value = { ...dealDictObjData({}, info.value), ...info.value }
   getSimilarPositionList()
@@ -312,7 +313,7 @@ const handleSubmit = async (val) =>{
 }
 
 const toDetails = (info) => {
-  let url = `/recruit/personal/message?id=${info.contact.userId}&job=${info.id}`
+  let url = `/recruit/personal/message?id=${info.id}`
   if (info.contact.enterpriseId) {
     url += `&enterprise=${info.contact.enterpriseId}`
   }