Просмотр исходного кода

【新增】:mall 客服实现 emoji 表情选择和消息发送

puhui999 11 месяцев назад
Родитель
Сommit
9c05ff35db

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

@@ -51,7 +51,7 @@ export interface KeFuMessageRespVO {
 export const KeFuMessageApi = {
   // 发送客服消息
   sendKeFuMessage: async (data: any) => {
-    return await request.put({
+    return await request.post({
       url: '/promotion/kefu-message/send',
       data
     })

+ 40 - 0
src/views/mall/promotion/kefu/components/EmojiSelectPopover.vue

@@ -0,0 +1,40 @@
+<!-- emoji 表情选择组件 -->
+<template>
+  <el-popover :width="500" placement="top" trigger="click">
+    <template #reference>
+      <Icon :size="30" class="ml-10px" icon="twemoji:grinning-face" />
+    </template>
+    <ElScrollbar height="300px">
+      <ul class="ml-2 flex flex-wrap px-2">
+        <li
+          v-for="(item, index) in emojiList"
+          :key="index"
+          :style="{
+            borderColor: 'var(--el-color-primary)',
+            color: 'var(--el-color-primary)'
+          }"
+          :title="item.name"
+          class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
+          @click="handleSelect(item)"
+        >
+          <img :src="item.url" style="width: 24px; height: 24px" />
+        </li>
+      </ul>
+    </ElScrollbar>
+  </el-popover>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'EmojiSelectPopover' })
+import { Emoji, getEmojiList } from './emoji'
+
+const emojiList = computed(() => getEmojiList())
+
+const emits = defineEmits<{
+  (e: 'select-emoji', v: Emoji)
+}>()
+const handleSelect = (item: Emoji) => {
+  // 整个 emoji 数据传递出去,方便以后输入框直接显示表情
+  emits('select-emoji', item)
+}
+</script>

+ 94 - 187
src/views/mall/promotion/kefu/components/KeFuChatBox.vue

@@ -3,58 +3,61 @@
     <el-header>
       <div class="kefu-title">{{ keFuConversation.userNickname }}</div>
     </el-header>
-    <el-main class="kefu-content">
-      <div
-        v-for="item in messageList"
-        :key="item.id"
-        :class="[
-          item.senderType === UserTypeEnum.MEMBER
-            ? `ss-row-left`
-            : item.senderType === UserTypeEnum.ADMIN
-              ? `ss-row-right`
-              : ''
-        ]"
-        class="flex mb-20px w-[100%]"
-      >
-        <el-avatar
-          v-show="item.senderType === UserTypeEnum.MEMBER"
-          :src="keFuConversation.userAvatar"
-          alt="avatar"
-        />
-        <div class="kefu-message flex items-center p-10px">
-          <!-- 文本消息 -->
-          <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
-            <div
-              v-dompurify-html="replaceEmoji(item.content)"
-              :class="[
-                item.senderType === UserTypeEnum.MEMBER
-                  ? `ml-10px`
-                  : item.senderType === UserTypeEnum.ADMIN
-                    ? `mr-10px`
-                    : ''
-              ]"
-            ></div>
-          </template>
-          <template v-else>
-            {{ item.content }}
-          </template>
+    <el-main class="kefu-content" style="overflow: visible">
+      <el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)">
+        <div ref="innerRef" class="w-[100%] pb-3px">
+          <div
+            v-for="item in messageList"
+            :key="item.id"
+            :class="[
+              item.senderType === UserTypeEnum.MEMBER
+                ? `ss-row-left`
+                : item.senderType === UserTypeEnum.ADMIN
+                  ? `ss-row-right`
+                  : ''
+            ]"
+            class="flex mb-20px w-[100%]"
+          >
+            <el-avatar
+              v-show="item.senderType === UserTypeEnum.MEMBER"
+              :src="keFuConversation.userAvatar"
+              alt="avatar"
+            />
+            <div class="kefu-message flex items-center p-10px">
+              <!-- 文本消息 -->
+              <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
+                <div
+                  v-dompurify-html="replaceEmoji(item.content)"
+                  :class="[
+                    item.senderType === UserTypeEnum.MEMBER
+                      ? `ml-10px`
+                      : item.senderType === UserTypeEnum.ADMIN
+                        ? `mr-10px`
+                        : ''
+                  ]"
+                ></div>
+              </template>
+              <template v-else>
+                {{ item.content }}
+              </template>
+            </div>
+            <el-avatar
+              v-show="item.senderType === UserTypeEnum.ADMIN"
+              :src="item.senderAvatar"
+              alt="avatar"
+            />
+          </div>
         </div>
-        <el-avatar
-          v-show="item.senderType === UserTypeEnum.ADMIN"
-          :src="item.senderAvatar"
-          alt="avatar"
-        />
-      </div>
+      </el-scrollbar>
     </el-main>
-    <!-- TODO puhui999: 发送下次提交完善 -->
     <el-footer height="230px">
       <div class="h-[100%]">
         <div class="chat-tools">
-          <Icon :size="30" class="ml-10px" icon="fa:frown-o" />
+          <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
         </div>
         <el-input v-model="message" :rows="6" type="textarea" />
         <div class="h-45px flex justify-end">
-          <el-button class="mt-10px" type="primary">发送</el-button>
+          <el-button class="mt-10px" type="primary" @click="handleSendMessage">发送</el-button>
         </div>
       </div>
     </el-footer>
@@ -63,162 +66,66 @@
 </template>
 
 <script lang="ts" setup>
-import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+import { ElScrollbar as ElScrollbarType } from 'element-plus'
+import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
 import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
-import { UserTypeEnum } from '@/utils/constants'
-import { replaceEmoji } from '@/views/mall/promotion/kefu/components/emoji'
-import { KeFuMessageContentTypeEnum } from '@/views/mall/promotion/kefu/components/constants'
+import EmojiSelectPopover from './EmojiSelectPopover.vue'
+import { Emoji, replaceEmoji } from './emoji'
+import { KeFuMessageContentTypeEnum } from './constants'
 import { isEmpty } from '@/utils/is'
+import { UserTypeEnum } from '@/utils/constants'
 
 defineOptions({ name: 'KeFuMessageBox' })
+const messageTool = useMessage()
 const message = ref('') // 消息
 const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
 const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
-// 获得消息
+
+// 获得消息 TODO puhui999:  先不考虑下拉加载历史消息
 const getMessageList = async (conversation: KeFuConversationRespVO) => {
   keFuConversation.value = conversation
-  // const { list } = await KeFuMessageApi.getKeFuMessagePage({
-  //   pageNo: 1,
-  //   conversationId: conversation.id
-  // })
-  // TODO puhui999: 方便查看效果
-  const list = [
-    {
-      id: 19,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 2,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[爱心][爱心][坏笑][坏笑][天使][天使]',
-      readStatus: false,
-      createTime: 1718616705000
-    },
-    {
-      id: 18,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[瞌睡][瞌睡]',
-      readStatus: false,
-      createTime: 1718616690000
-    },
-    {
-      id: 17,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[冷酷][冷酷]',
-      readStatus: false,
-      createTime: 1718616350000
-    },
-    {
-      id: 16,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[天使]',
-      readStatus: false,
-      createTime: 1718615505000
-    },
-    {
-      id: 15,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 2,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[天使]',
-      readStatus: false,
-      createTime: 1718615485000
-    },
-    {
-      id: 14,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 2,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[心碎][心碎]',
-      readStatus: false,
-      createTime: 1718615453000
-    },
-    {
-      id: 13,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 2,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[心碎][心碎]',
-      readStatus: false,
-      createTime: 1718615430000
-    },
-    {
-      id: 12,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[心碎][心碎]',
-      readStatus: false,
-      createTime: 1718615425000
-    },
-    {
-      id: 11,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[困~][困~]',
-      readStatus: false,
-      createTime: 1718615413000
-    },
-    {
-      id: 10,
-      conversationId: 1,
-      senderId: 283,
-      senderAvatar: null,
-      senderType: 1,
-      receiverId: null,
-      receiverType: null,
-      contentType: 1,
-      content: '[睡着][睡着]',
-      readStatus: false,
-      createTime: 1718615407000
-    }
-  ]
-  messageList.value = list
+  const { list } = await KeFuMessageApi.getKeFuMessagePage({
+    pageNo: 1,
+    conversationId: conversation.id
+  })
+  messageList.value = list.reverse()
+  // TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
+  await scrollToBottom()
 }
 defineExpose({ getMessageList })
 // 是否显示聊天区域
 const showChatBox = computed(() => !isEmpty(keFuConversation.value))
+// 处理表情选择
+const handleEmojiSelect = (item: Emoji) => {
+  message.value += item.name
+}
+// 发送消息
+const handleSendMessage = async () => {
+  // 1. 校验消息是否为空
+  if (isEmpty(unref(message.value))) {
+    messageTool.warning('请输入消息后再发送哦!')
+  }
+  // 2. 组织发送消息
+  const msg = {
+    conversationId: keFuConversation.value.id,
+    contentType: KeFuMessageContentTypeEnum.TEXT,
+    content: message.value
+  }
+  await KeFuMessageApi.sendKeFuMessage(msg)
+  message.value = ''
+  // 3. 加载消息列表
+  await getMessageList(keFuConversation.value)
+  // 滚动到最新消息处
+  await scrollToBottom()
+}
+
+const innerRef = ref<HTMLDivElement>()
+const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
+// 滚动到底部
+const scrollToBottom = async () => {
+  await nextTick()
+  scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
+}
 </script>
 
 <style lang="scss" scoped>

+ 16 - 1
src/views/mall/promotion/kefu/components/emoji.ts

@@ -49,6 +49,11 @@ export const emojiList = [
   { name: '[恶魔]', file: 'emo.png' }
 ]
 
+export interface Emoji {
+  name: string
+  url: string
+}
+
 export const emojiPage = {}
 emojiList.forEach((item, index) => {
   if (!emojiPage[Math.floor(index / 30) + 1]) {
@@ -59,6 +64,8 @@ emojiList.forEach((item, index) => {
 
 // 后端上传地址
 const staticUrl = import.meta.env.VITE_STATIC_URL
+// 后缀
+const suffix = '/static/img/chat/emoji/'
 
 // 处理表情
 export function replaceEmoji(data: string) {
@@ -72,7 +79,7 @@ export function replaceEmoji(data: string) {
         newData = newData.replace(
           item,
           `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${
-            staticUrl + '/static/img/chat/emoji/' + emojiFile
+            staticUrl + suffix + emojiFile
           }"/>`
         )
       })
@@ -81,6 +88,14 @@ export function replaceEmoji(data: string) {
   return newData
 }
 
+// 获得所有表情
+export function getEmojiList(): Emoji[] {
+  return emojiList.map((item) => ({
+    url: staticUrl + suffix + item.file,
+    name: item.name
+  })) as Emoji[]
+}
+
 function selEmojiFile(name: string) {
   for (const index in emojiList) {
     if (emojiList[index].name === name) {