Browse Source

!471 mall 客服会话相关
Merge pull request !471 from puhui999/dev-crm

芋道源码 11 months ago
parent
commit
8d73d741d4

+ 12 - 49
src/api/mall/promotion/kefu/conversation/index.ts

@@ -1,55 +1,18 @@
 import request from '@/config/axios'
 
-// TODO @puhui999:注释要不放在属性后面,避免太长哈
 export interface KeFuConversationRespVO {
-  /**
-   * 编号
-   */
-  id: number
-  /**
-   * 会话所属用户
-   */
-  userId: number
-  /**
-   * 会话所属用户头像
-   */
-  userAvatar: string
-  /**
-   * 会话所属用户昵称
-   */
-  userNickname: string
-  /**
-   * 最后聊天时间
-   */
-  lastMessageTime: Date
-  /**
-   * 最后聊天内容
-   */
-  lastMessageContent: string
-  /**
-   * 最后发送的消息类型
-   */
-  lastMessageContentType: number
-  /**
-   * 管理端置顶
-   */
-  adminPinned: boolean
-  /**
-   * 用户是否可见
-   */
-  userDeleted: boolean
-  /**
-   * 管理员是否可见
-   */
-  adminDeleted: boolean
-  /**
-   * 管理员未读消息数
-   */
-  adminUnreadMessageCount: number
-  /**
-   * 创建时间
-   */
-  createTime?: string
+  id: number // 编号
+  userId: number // 会话所属用户
+  userAvatar: string // 会话所属用户头像
+  userNickname: string // 会话所属用户昵称
+  lastMessageTime: Date // 最后聊天时间
+  lastMessageContent: string // 最后聊天内容
+  lastMessageContentType: number // 最后发送的消息类型
+  adminPinned: boolean // 管理端置顶
+  userDeleted: boolean // 用户是否可见
+  adminDeleted: boolean // 管理员是否可见
+  adminUnreadMessageCount: number // 管理员未读消息数
+  createTime?: string // 创建时间
 }
 
 // 客服会话 API

+ 13 - 47
src/api/mall/promotion/kefu/message/index.ts

@@ -1,50 +1,17 @@
 import request from '@/config/axios'
 
 export interface KeFuMessageRespVO {
-  /**
-   * 编号
-   */
-  id: number
-  /**
-   * 会话编号
-   */
-  conversationId: number
-  /**
-   * 发送人编号
-   */
-  senderId: number
-  /**
-   * 发送人头像
-   */
-  senderAvatar: string
-  /**
-   * 发送人类型
-   */
-  senderType: number
-  /**
-   * 接收人编号
-   */
-  receiverId: number
-  /**
-   * 接收人类型
-   */
-  receiverType: number
-  /**
-   * 消息类型
-   */
-  contentType: number
-  /**
-   * 消息
-   */
-  content: string
-  /**
-   * 是否已读
-   */
-  readStatus: boolean
-  /**
-   * 创建时间
-   */
-  createTime: Date
+  id: number // 编号
+  conversationId: number // 会话编号
+  senderId: number // 发送人编号
+  senderAvatar: string // 发送人头像
+  senderType: number // 发送人类型
+  receiverId: number // 接收人编号
+  receiverType: number // 接收人类型
+  contentType: number // 消息类型
+  content: string // 消息
+  readStatus: boolean // 是否已读
+  createTime: Date // 创建时间
 }
 
 // 客服会话 API
@@ -57,10 +24,9 @@ export const KeFuMessageApi = {
     })
   },
   // 更新客服消息已读状态
-  updateKeFuMessageReadStatus: async (data: any) => {
+  updateKeFuMessageReadStatus: async (conversationId: number) => {
     return await request.put({
-      url: '/promotion/kefu-message/update-read-status',
-      data
+      url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
     })
   },
   // 获得消息分页数据

+ 7 - 3
src/views/mall/promotion/kefu/components/KeFuChatBox.vue

@@ -18,12 +18,12 @@
                 {{ formatDate(item.createTime) }}
               </div>
               <!-- 系统消息 -->
-              <view
+              <div
                 v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
                 class="system-message"
               >
                 {{ item.content }}
-              </view>
+              </div>
             </div>
             <div
               :class="[
@@ -154,9 +154,10 @@ const handleSendMessage = async () => {
 
 // 发送消息 【共用】
 const sendMessage = async (msg: any) => {
+  // 发送消息
   await KeFuMessageApi.sendKeFuMessage(msg)
   message.value = ''
-  // 3. 加载消息列表
+  // 加载消息列表
   await getMessageList(keFuConversation.value)
   // 滚动到最新消息处
   await scrollToBottom()
@@ -166,8 +167,11 @@ const innerRef = ref<HTMLDivElement>()
 const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
 // 滚动到底部
 const scrollToBottom = async () => {
+  // 1. 滚动到最新消息
   await nextTick()
   scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
+  // 2. 消息已读
+  await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id)
 }
 /**
  * 是否显示时间

+ 155 - 6
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue

@@ -3,12 +3,21 @@
     <div
       v-for="(item, index) in conversationList"
       :key="item.id"
-      :class="{ active: index === activeConversationIndex }"
-      class="kefu-conversation flex justify-between items-center"
+      :class="{ active: index === activeConversationIndex, pinned: item.adminPinned }"
+      class="kefu-conversation flex items-center"
       @click="openRightMessage(item, index)"
+      @contextmenu.prevent="rightClick($event as PointerEvent, item)"
     >
       <div class="flex justify-center items-center w-100%">
-        <el-avatar :src="item.userAvatar" alt="avatar" />
+        <div class="flex justify-center items-center" style="width: 50px; height: 50px">
+          <el-badge
+            :hidden="item.adminUnreadMessageCount === 0"
+            :max="99"
+            :value="item.adminUnreadMessageCount"
+          >
+            <el-avatar :src="item.userAvatar" alt="avatar" />
+          </el-badge>
+        </div>
         <div class="ml-10px w-100%">
           <div class="flex justify-between items-center w-100%">
             <span>{{ item.userNickname }}</span>
@@ -24,27 +33,75 @@
             ></div>
           </template>
           <!-- 图片消息 -->
-          <template v-if="KeFuMessageContentTypeEnum.IMAGE === item.lastMessageContentType">
-            <div class="last-message flex items-center color-[#989EA6]">【图片消息】</div>
+          <template v-else>
+            <div class="last-message flex items-center color-[#989EA6]">
+              {{ getContentType(item.lastMessageContentType) }}
+            </div>
           </template>
         </div>
       </div>
     </div>
+    <!-- 通过右击获取到的坐标定位 -->
+    <ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
+      <li
+        v-show="!selectedConversation.adminPinned"
+        class="flex items-center"
+        @click.stop="updateConversationPinned(true)"
+      >
+        <Icon class="mr-5px" icon="ep:top" />
+        置顶会话
+      </li>
+      <li
+        v-show="selectedConversation.adminPinned"
+        class="flex items-center"
+        @click.stop="updateConversationPinned(false)"
+      >
+        <Icon class="mr-5px" icon="ep:bottom" />
+        取消置顶
+      </li>
+      <li class="flex items-center" @click.stop="deleteConversation">
+        <Icon class="mr-5px" color="red" icon="ep:delete" />
+        删除会话
+      </li>
+      <li class="flex items-center" @click.stop="closeRightMenu">
+        <Icon class="mr-5px" color="red" icon="ep:close" />
+        取消
+      </li>
+    </ul>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { useEmoji } from './tools/emoji'
-import { formatDate } from '@/utils/formatTime'
+import { formatDate, getNowDateTime } from '@/utils/formatTime'
 import { KeFuMessageContentTypeEnum } from './tools/constants'
 
 defineOptions({ name: 'KeFuConversationBox' })
+const message = useMessage()
 const { replaceEmoji } = useEmoji()
 const activeConversationIndex = ref(-1) // 选中的会话
 const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
 const getConversationList = async () => {
   conversationList.value = await KeFuConversationApi.getConversationList()
+  // 测试数据
+  for (let i = 0; i < 5; i++) {
+    conversationList.value.push({
+      id: 1,
+      userId: 283,
+      userAvatar:
+        'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132',
+      userNickname: '辉辉鸭' + i,
+      lastMessageTime: getNowDateTime(),
+      lastMessageContent:
+        '[爱心][爱心]你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇',
+      lastMessageContentType: 1,
+      adminPinned: false,
+      userDeleted: false,
+      adminDeleted: false,
+      adminUnreadMessageCount: i
+    })
+  }
 }
 defineExpose({ getConversationList })
 const emits = defineEmits<{
@@ -55,6 +112,73 @@ const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
   activeConversationIndex.value = index
   emits('change', item)
 }
+// 获得消息类型
+const getContentType = computed(() => (lastMessageContentType: number) => {
+  switch (lastMessageContentType) {
+    case KeFuMessageContentTypeEnum.SYSTEM:
+      return '[系统消息]'
+    case KeFuMessageContentTypeEnum.VIDEO:
+      return '[视频消息]'
+    case KeFuMessageContentTypeEnum.IMAGE:
+      return '[图片消息]'
+    case KeFuMessageContentTypeEnum.PRODUCT:
+      return '[商品消息]'
+    case KeFuMessageContentTypeEnum.ORDER:
+      return '[订单消息]'
+    case KeFuMessageContentTypeEnum.VOICE:
+      return '[语音消息]'
+    default:
+      return ''
+  }
+})
+
+//======================= 右键菜单 =======================
+const showRightMenu = ref(false) // 显示右键菜单
+const rightMenuStyle = ref<any>({}) // 右键菜单 Style
+const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 右键选中的会话对象
+// 右键菜单
+const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
+  selectedConversation.value = item
+  // 显示右键菜单
+  showRightMenu.value = true
+  rightMenuStyle.value = {
+    top: mouseEvent.clientY - 110 + 'px',
+    left: mouseEvent.clientX - 80 + 'px'
+  }
+}
+// 关闭菜单
+const closeRightMenu = () => {
+  showRightMenu.value = false
+}
+// 置顶会话
+const updateConversationPinned = async (adminPinned: boolean) => {
+  // 1. 会话置顶/取消置顶
+  await KeFuConversationApi.updateConversationPinned({
+    id: selectedConversation.value.id,
+    adminPinned
+  })
+  // TODO puhui999: 快速操作两次提示只会提示一次看看怎么优雅解决
+  message.success(adminPinned ? '置顶成功' : '取消置顶成功')
+  // 2. 关闭右键菜单,更新会话列表
+  closeRightMenu()
+  await getConversationList()
+}
+// 删除会话
+const deleteConversation = async () => {
+  // 1. 删除会话
+  await message.confirm('您确定要删除该会话吗?')
+  await KeFuConversationApi.deleteConversation(selectedConversation.value.id)
+  // 2. 关闭右键菜单,更新会话列表
+  closeRightMenu()
+  await getConversationList()
+}
+watch(showRightMenu, (val) => {
+  if (val) {
+    document.body.addEventListener('click', closeRightMenu)
+  } else {
+    document.body.removeEventListener('click', closeRightMenu)
+  }
+})
 </script>
 
 <style lang="scss" scoped>
@@ -77,5 +201,30 @@ const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
     border-left: 5px #3271ff solid;
     background-color: #eff0f1;
   }
+
+  .pinned {
+    background-color: #eff0f1;
+  }
+
+  .right-menu-ul {
+    position: absolute;
+    background-color: #fff;
+    padding: 10px;
+    margin: 0;
+    list-style-type: none; /* 移除默认的项目符号 */
+    border-radius: 12px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */
+    width: 130px;
+
+    li {
+      padding: 8px 16px;
+      cursor: pointer;
+      border-radius: 12px;
+      transition: background-color 0.3s; /* 平滑过渡 */
+      &:hover {
+        background-color: #e0e0e0; /* 悬停时的背景颜色 */
+      }
+    }
+  }
 }
 </style>

+ 1 - 2
src/views/mall/promotion/kefu/components/index.ts

@@ -1,5 +1,4 @@
 import KeFuConversationBox from './KeFuConversationBox.vue'
 import KeFuChatBox from './KeFuChatBox.vue'
-import * as Constants from './tools/constants'
 
-export { KeFuConversationBox, KeFuChatBox, Constants }
+export { KeFuConversationBox, KeFuChatBox }

+ 9 - 7
src/views/mall/promotion/kefu/index.vue

@@ -7,7 +7,7 @@
     </el-col>
     <el-col :span="16">
       <ContentWrap>
-        <KeFuChatBox ref="keFuChatBoxRef" />
+        <KeFuChatBox ref="keFuChatBoxRef" @change="getConversationList" />
       </ContentWrap>
     </el-col>
   </el-row>
@@ -15,13 +15,14 @@
 
 <script lang="ts" setup>
 import { KeFuChatBox, KeFuConversationBox } from './components'
+import { WebSocketMessageTypeConstants } from './components/tools/constants'
 import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { getAccessToken } from '@/utils/auth'
 import { useWebSocket } from '@vueuse/core'
-import { WebSocketMessageTypeConstants } from '@/views/mall/promotion/kefu/components/tools/constants'
 
 defineOptions({ name: 'KeFu' })
 const message = useMessage()
+
 // 加载消息
 const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
 const handleChange = (conversation: KeFuConversationRespVO) => {
@@ -47,10 +48,6 @@ watchEffect(() => {
   try {
     // 1. 收到心跳
     if (data.value === 'pong') {
-      // state.recordList.push({
-      //   text: '【心跳】',
-      //   time: new Date().getTime()
-      // })
       return
     }
 
@@ -63,12 +60,17 @@ watchEffect(() => {
     }
     // 2.2 消息类型:KEFU_MESSAGE_TYPE
     if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
-      // 刷新列表
+      // 刷新会话列表
       getConversationList()
       // 刷新消息列表
       keFuChatBoxRef.value?.refreshMessageList()
       return
     }
+    // 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
+    if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
+      // 刷新会话列表
+      getConversationList()
+    }
   } catch (error) {
     console.error(error)
   }