gaoluyang
2026-06-24 712aa51536236d43e87273e4ce45ac5691dffad8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<script lang="ts" setup>
import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation';
 
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
 
import { Page } from '..\..\..\..\packages\effects\common-ui\src';
import { WebSocketMessageTypeConstants } from '..\..\..\..\packages\constants\src';
import { useAccessStore } from '..\..\..\..\packages\stores\src';
 
import { useWebSocket } from '@vueuse/core';
import { message } from 'ant-design-vue';
 
import { useMallKefuStore } from '#/store/mall/kefu';
 
import ConversationList from './modules/conversation-list.vue';
import MemberInfo from './modules/member/member-info.vue';
import MessageList from './modules/message-list.vue';
 
const accessStore = useAccessStore();
const kefuStore = useMallKefuStore(); // 客服缓存
 
/** 组件引用 */
const messageListRef = ref<InstanceType<typeof MessageList>>();
const memberInfoRef = ref<InstanceType<typeof MemberInfo>>();
const conversationListRef = ref<InstanceType<typeof ConversationList>>();
 
// ======================= WebSocket start =======================
const url = `${`${import.meta.env.VITE_BASE_URL}/infra/ws`.replace(
  'http',
  'ws',
)}?token=${accessStore.refreshToken}`; // 使用 refreshToken() :WebSocket 无法方便的刷新访问令牌
const server = ref(url); // WebSocket 服务地址
 
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
  autoReconnect: true,
  heartbeat: true,
});
 
/** 监听 WebSocket 数据 */
watch(
  () => data.value,
  (newData) => {
    if (!newData) return;
    try {
      // 1. 收到心跳
      if (newData === 'pong') return;
 
      // 2.1 解析 type 消息类型
      const jsonMessage = JSON.parse(newData);
      const type = jsonMessage.type;
      if (!type) {
        message.error(`未知的消息类型:${newData}`);
        return;
      }
 
      // 2.2 消息类型:KEFU_MESSAGE_TYPE
      if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
        const message = JSON.parse(jsonMessage.content);
        // 刷新会话列表
        kefuStore.updateConversation(message.conversationId);
        // 刷新消息列表
        messageListRef.value?.refreshMessageList(message);
        return;
      }
 
      // 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
      if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
        // 更新会话已读
        const message = JSON.parse(jsonMessage.content);
        kefuStore.updateConversationStatus(message.conversationId);
      }
    } catch (error) {
      console.error(error);
    }
  },
  {
    immediate: false, // 不立即执行
  },
);
// ======================= WebSocket end =======================
 
function handleChange(conversation: MallKefuConversationApi.Conversation) {
  messageListRef.value?.getNewMessageList(conversation);
  memberInfoRef.value?.initHistory(conversation);
}
 
/** 初始化 */
onMounted(() => {
  // 加载会话列表
  kefuStore.setConversationList().then(() => {
    conversationListRef.value?.calculationLastMessageTime();
  });
  // 打开 websocket 连接
  open();
});
 
/** 销毁 */
onBeforeUnmount(() => {
  // 关闭 websocket 连接
  close();
});
</script>
 
<template>
  <Page auto-content-height>
    <div class="flex h-full antialiased">
      <div class="flex h-full w-full flex-row overflow-x-hidden">
        <!-- 会话列表 -->
        <ConversationList
          class="w-1/6"
          ref="conversationListRef"
          @change="handleChange"
        />
        <!-- 会话详情(选中会话的消息列表) -->
        <MessageList class="w-4/6" ref="messageListRef" />
        <!-- 会员信息(选中会话的会员信息) -->
        <MemberInfo class="w-[280px]" ref="memberInfoRef" />
      </div>
    </div>
  </Page>
</template>