Browse Source

!576 【功能完善】商城客服
Merge pull request !576 from puhui999/dev-crm

芋道源码 9 months ago
parent
commit
d3f3044655

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

@@ -29,8 +29,8 @@ export const KeFuMessageApi = {
       url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
     })
   },
-  // 获得消息分页数据
-  getKeFuMessagePage: async (params: any) => {
-    return await request.get({ url: '/promotion/kefu-message/page', params })
+  // 获得消息数据
+  getKeFuMessageList: async (params: any) => {
+    return await request.get({ url: '/promotion/kefu-message/list', params })
   }
 }

+ 37 - 0
src/store/modules/mall/kefu.ts

@@ -0,0 +1,37 @@
+import { store } from '@/store'
+import { defineStore } from 'pinia'
+import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+
+// TODO puhui999: 待优化完善
+interface MallKefuInfoVO {
+  conversationList: KeFuConversationRespVO[] // 会话列表
+  conversationMessageList: Map<number, KeFuMessageRespVO[]> // 会话消息
+}
+
+export const useMallKefuStore = defineStore('mall-kefu', {
+  state: (): MallKefuInfoVO => ({
+    conversationList: [],
+    conversationMessageList: new Map<number, KeFuMessageRespVO[]>() // key 会话,value 会话消息列表
+  }),
+  getters: {
+    getConversationList(): KeFuConversationRespVO[] {
+      return this.conversationList
+    },
+    getConversationMessageList(): Map<number, KeFuMessageRespVO[]> {
+      return this.conversationMessageList
+    }
+  },
+  actions: {
+    async setConversationList() {
+      const list = await KeFuConversationApi.getConversationList()
+      list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
+      this.conversationList = list
+    }
+    // async setConversationMessageList(conversationId: number) {}
+  }
+})
+
+export const useUserStoreWithOut = () => {
+  return useMallKefuStore(store)
+}

+ 11 - 12
src/views/mall/promotion/kefu/components/KeFuConversationList.vue

@@ -1,5 +1,6 @@
 <template>
-  <div class="kefu">
+  <el-aside class="kefu p-5px h-100%" width="260px">
+    <div class="color-[#999] font-bold my-10px">会话记录({{ conversationList.length }})</div>
     <div
       v-for="item in conversationList"
       :key="item.id"
@@ -22,7 +23,7 @@
         <div class="ml-10px w-100%">
           <div class="flex justify-between items-center w-100%">
             <span class="username">{{ item.userNickname }}</span>
-            <span class="color-[var(--left-menu-text-color)]" style="font-size: 13px">
+            <span class="color-[#999]" style="font-size: 13px">
               {{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
             </span>
           </div>
@@ -31,7 +32,7 @@
             v-dompurify-html="
               getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
             "
-            class="last-message flex items-center color-[var(--left-menu-text-color)]"
+            class="last-message flex items-center color-[#999]"
           >
           </div>
         </div>
@@ -65,7 +66,7 @@
         取消
       </li>
     </ul>
-  </div>
+  </el-aside>
 </template>
 
 <script lang="ts" setup>
@@ -180,11 +181,12 @@ watch(showRightMenu, (val) => {
 
 <style lang="scss" scoped>
 .kefu {
+  background-color: #fff;
+
   &-conversation {
     height: 60px;
-    padding: 10px;
     //background-color: #fff;
-    transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
+    //transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
 
     .username {
       min-width: 0;
@@ -205,13 +207,10 @@ watch(showRightMenu, (val) => {
     }
   }
 
-  .active {
-    border-left: 5px #3271ff solid;
-    background-color: var(--login-bg-color);
-  }
-
+  .active,
   .pinned {
-    background-color: var(--left-menu-bg-active-color);
+    background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
+    border-radius: 8px;
   }
 
   .right-menu-ul {

+ 102 - 82
src/views/mall/promotion/kefu/components/KeFuMessageList.vue

@@ -1,11 +1,11 @@
 <template>
   <el-container v-if="showKeFuMessageList" class="kefu">
-    <el-header>
+    <el-header class="kefu-header">
       <div class="kefu-title">{{ conversation.userNickname }}</div>
     </el-header>
     <el-main class="kefu-content overflow-visible">
-      <el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
-        <div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
+      <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
+        <div v-if="refreshContent" ref="innerRef" class="w-[100%] px-10px">
           <!-- 消息列表 -->
           <div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
             <div class="flex justify-center items-center mb-20px">
@@ -43,7 +43,9 @@
                 class="w-60px h-60px"
               />
               <div
-                :class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
+                :class="{
+                  'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType
+                }"
                 class="p-10px"
               >
                 <!-- 文本消息 -->
@@ -71,10 +73,10 @@
                 <MessageItem :message="item">
                   <ProductItem
                     v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
-                    :spuId="getMessageContent(item).spuId"
                     :picUrl="getMessageContent(item).picUrl"
                     :price="getMessageContent(item).price"
                     :skuText="getMessageContent(item).introduction"
+                    :spuId="getMessageContent(item).spuId"
                     :title="getMessageContent(item).spuName"
                     :titleWidth="400"
                     class="max-w-70%"
@@ -108,23 +110,29 @@
         <Icon class="ml-5px" icon="ep:bottom" />
       </div>
     </el-main>
-    <el-footer height="230px">
-      <div class="h-[100%]">
-        <div class="chat-tools flex items-center">
-          <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
-          <PictureSelectUpload
-            class="ml-15px mt-3px cursor-pointer"
-            @send-picture="handleSendPicture"
-          />
-        </div>
-        <el-input v-model="message" :rows="6" style="border-style: none" type="textarea" />
-        <div class="h-45px flex justify-end">
-          <el-button class="mt-10px" type="primary" @click="handleSendMessage">发送</el-button>
-        </div>
+    <el-footer class="kefu-footer">
+      <div class="chat-tools flex items-center">
+        <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
+        <PictureSelectUpload
+          class="ml-15px mt-3px cursor-pointer"
+          @send-picture="handleSendPicture"
+        />
       </div>
+      <el-input
+        v-model="message"
+        :rows="6"
+        placeholder="输入消息,Enter发送,Shift+Enter换行"
+        style="border-style: none"
+        type="textarea"
+        @keyup.enter.prevent="handleSendMessage"
+      />
     </el-footer>
   </el-container>
-  <el-empty v-else description="请选择左侧的一个会话后开始" />
+  <el-container v-else class="kefu">
+    <el-main>
+      <el-empty description="请选择左侧的一个会话后开始" />
+    </el-main>
+  </el-container>
 </template>
 
 <script lang="ts" setup>
@@ -156,9 +164,8 @@ const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
 const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
 const showNewMessageTip = ref(false) // 显示有新消息提示
 const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10,
-  conversationId: 0
+  conversationId: 0,
+  createTime: undefined
 })
 const total = ref(0) // 消息总条数
 const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
@@ -167,14 +174,20 @@ const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面
 const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
 /** 获得消息列表 */
 const getMessageList = async () => {
-  const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
-  total.value = res.total
+  const res = await KeFuMessageApi.getKeFuMessageList(queryParams)
+  if (isEmpty(res)) {
+    // 当返回的是空列表说明没有消息或者已经查询完了历史消息
+    skipGetMessageList.value = true
+    return
+  }
+  queryParams.createTime = formatDate(res.at(-1).createTime) as any
+
   // 情况一:加载最新消息
-  if (queryParams.pageNo === 1) {
-    messageList.value = res.list
+  if (!queryParams.createTime) {
+    messageList.value = res
   } else {
     // 情况二:加载历史消息
-    for (const item of res.list) {
+    for (const item of res) {
       pushMessage(item)
     }
   }
@@ -208,8 +221,7 @@ const refreshMessageList = async (message?: any) => {
     }
     pushMessage(message)
   } else {
-    // TODO @puhui999:不基于 page 做。而是流式分页;通过 createTime 排序查询;
-    queryParams.pageNo = 1
+    queryParams.createTime = undefined
     await getMessageList()
   }
 
@@ -226,7 +238,6 @@ const refreshMessageList = async (message?: any) => {
 // TODO @puhui999:可优化:可以考虑本地做每个会话的消息 list 缓存;然后点击切换时,读取缓存;然后异步获取新消息,merge 下;
 const getNewMessageList = async (val: KeFuConversationRespVO) => {
   // 会话切换,重置相关参数
-  queryParams.pageNo = 1
   messageList.value = []
   total.value = 0
   loadHistory.value = false
@@ -234,16 +245,14 @@ const getNewMessageList = async (val: KeFuConversationRespVO) => {
   // 设置会话相关属性
   conversation.value = val
   queryParams.conversationId = val.id
+  queryParams.createTime = undefined
   // 获取消息
   await refreshMessageList()
 }
 defineExpose({ getNewMessageList, refreshMessageList })
 
 const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
-const skipGetMessageList = computed(() => {
-  // 已加载到最后一页的话则不触发新的消息获取
-  return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
-}) // 跳过消息获取
+const skipGetMessageList = ref(false) // 跳过消息获取
 
 /** 处理表情选择 */
 const handleEmojiSelect = (item: Emoji) => {
@@ -262,7 +271,11 @@ const handleSendPicture = async (picUrl: string) => {
 }
 
 /** 发送文本消息 */
-const handleSendMessage = async () => {
+const handleSendMessage = async (event: any) => {
+  // shift 不发送
+  if (event.shiftKey) {
+    return
+  }
   // 1. 校验消息是否为空
   if (isEmpty(unref(message.value))) {
     messageTool.notifyWarning('请输入消息后再发送哦!')
@@ -333,8 +346,6 @@ const handleOldMessage = async () => {
     return
   }
   loadHistory.value = true
-  // 加载消息列表
-  queryParams.pageNo += 1
   await getMessageList()
   // 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
   scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
@@ -357,14 +368,29 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
 
 <style lang="scss" scoped>
 .kefu {
-  &-title {
-    border-bottom: #e4e0e0 solid 1px;
-    height: 60px;
-    line-height: 60px;
+  background-color: #fff;
+  width: calc(100% - 300px - 260px);
+  border-left: var(--el-border-color) solid 1px;
+
+  .kefu-header {
+    background: #fbfbfb;
+    box-shadow: 0 0 0 0 #dcdfe6;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    &-title {
+      font-size: 18px;
+      font-weight: bold;
+    }
   }
 
   &-content {
+    margin: 0;
+    padding: 0;
     position: relative;
+    height: 100%;
+    width: 100%;
 
     .newMessageTip {
       position: absolute;
@@ -381,21 +407,12 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
       justify-content: flex-start;
 
       .kefu-message {
-        margin-left: 20px;
-        position: relative;
-
-        &::before {
-          content: '';
-          width: 10px;
-          height: 10px;
-          left: -19px;
-          top: calc(50% - 10px);
-          position: absolute;
-          border-left: 5px solid transparent;
-          border-bottom: 5px solid transparent;
-          border-top: 5px solid transparent;
-          border-right: 5px solid var(--app-content-bg-color);
-        }
+        background-color: rgb(245, 245, 245);
+        margin-left: 10px;
+        margin-top: 18px;
+        border-top-right-radius: 10px;
+        border-bottom-right-radius: 10px;
+        border-bottom-left-radius: 10px;
       }
     }
 
@@ -403,37 +420,25 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
       justify-content: flex-end;
 
       .kefu-message {
-        margin-right: 20px;
-        position: relative;
-
-        &::after {
-          content: '';
-          width: 10px;
-          height: 10px;
-          right: -19px;
-          top: calc(50% - 10px);
-          position: absolute;
-          border-left: 5px solid var(--app-content-bg-color);
-          border-bottom: 5px solid transparent;
-          border-top: 5px solid transparent;
-          border-right: 5px solid transparent;
-        }
+        background-color: rgb(206, 223, 255);
+        margin-right: 10px;
+        margin-top: 18px;
+        border-top-left-radius: 10px;
+        border-bottom-right-radius: 10px;
+        border-bottom-left-radius: 10px;
       }
     }
 
     // 消息气泡
     .kefu-message {
-      color: #a9a9a9;
-      border-radius: 5px;
-      box-shadow: 3px 3px 5px rgba(220, 220, 220, 0.1);
+      color: #414141;
+      font-weight: 500;
       padding: 5px 10px;
       width: auto;
       max-width: 50%;
       text-align: left;
       display: inline-block !important;
-      position: relative;
       word-break: break-all;
-      background-color: var(--app-content-bg-color);
       transition: all 0.2s;
 
       &:hover {
@@ -447,21 +452,36 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
       border-radius: 12rpx;
       padding: 8rpx 16rpx;
       margin-bottom: 16rpx;
-      //background-color: #e8e8e8;
       color: #999;
       font-size: 24rpx;
     }
   }
 
-  .chat-tools {
-    width: 100%;
-    border: var(--el-border-color) solid 1px;
-    border-radius: 10px;
-    height: 44px;
+  .kefu-footer {
+    display: flex;
+    flex-direction: column;
+    height: auto;
+    margin: 0;
+    padding: 0;
+    border-top: var(--el-border-color) solid 1px;
+
+    .chat-tools {
+      width: 100%;
+      height: 44px;
+    }
   }
 
   ::v-deep(textarea) {
     resize: none;
   }
+
+  :deep(.el-input__wrapper) {
+    box-shadow: none !important;
+    border-radius: 0;
+  }
+
+  ::v-deep(.el-textarea__inner) {
+    box-shadow: none !important;
+  }
 }
 </style>

+ 0 - 97
src/views/mall/promotion/kefu/components/history/MemberBrowsingHistory.vue

@@ -1,97 +0,0 @@
-<!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 -->
-<template>
-  <div v-show="!isEmpty(conversation)" class="kefu">
-    <div class="header-title h-60px flex justify-center items-center">他的足迹</div>
-    <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
-      <el-tab-pane label="最近浏览" name="a" />
-      <el-tab-pane label="订单列表" name="b" />
-    </el-tabs>
-    <div>
-      <el-scrollbar ref="scrollbarRef" always height="calc(115vh - 400px)" @scroll="handleScroll">
-        <!-- 最近浏览 -->
-        <ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" />
-        <!-- 订单列表 -->
-        <OrderBrowsingHistory v-if="activeName === 'b'" ref="orderBrowsingHistoryRef" />
-      </el-scrollbar>
-    </div>
-  </div>
-  <el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
-</template>
-
-<script lang="ts" setup>
-import type { TabsPaneContext } from 'element-plus'
-import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
-import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
-import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
-import { isEmpty } from '@/utils/is'
-import { debounce } from 'lodash-es'
-import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index'
-
-defineOptions({ name: 'MemberBrowsingHistory' })
-
-const activeName = ref('a')
-
-/** tab 切换 */
-const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
-const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
-const handleClick = async (tab: TabsPaneContext) => {
-  activeName.value = tab.paneName as string
-  await nextTick()
-  await getHistoryList()
-}
-
-/** 获得历史数据 */
-// TODO @puhui:不要用 a、b 哈。就订单列表、浏览列表这种噶
-const getHistoryList = async () => {
-  switch (activeName.value) {
-    case 'a':
-      await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
-      break
-    case 'b':
-      await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
-      break
-    default:
-      break
-  }
-}
-
-/** 加载下一页数据 */
-const loadMore = async () => {
-  switch (activeName.value) {
-    case 'a':
-      await productBrowsingHistoryRef.value?.loadMore()
-      break
-    case 'b':
-      await orderBrowsingHistoryRef.value?.loadMore()
-      break
-    default:
-      break
-  }
-}
-
-/** 浏览历史初始化 */
-const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
-const initHistory = async (val: KeFuConversationRespVO) => {
-  activeName.value = 'a'
-  conversation.value = val
-  await nextTick()
-  await getHistoryList()
-}
-defineExpose({ initHistory })
-
-/** 处理消息列表滚动事件(debounce 限流) */
-const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
-const handleScroll = debounce(() => {
-  const wrap = scrollbarRef.value?.wrapRef
-  // 触底重置
-  if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
-    loadMore()
-  }
-}, 200)
-</script>
-
-<style lang="scss" scoped>
-.header-title {
-  border-bottom: #e4e0e0 solid 1px;
-}
-</style>

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

@@ -1,5 +1,5 @@
 import KeFuConversationList from './KeFuConversationList.vue'
 import KeFuMessageList from './KeFuMessageList.vue'
-import MemberBrowsingHistory from './history/MemberBrowsingHistory.vue'
+import MemberInfo from './member/MemberInfo.vue'
 
-export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory }
+export { KeFuConversationList, KeFuMessageList, MemberInfo }

+ 179 - 0
src/views/mall/promotion/kefu/components/member/MemberInfo.vue

@@ -0,0 +1,179 @@
+<!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 -->
+<template>
+  <el-container class="kefu">
+    <el-header class="kefu-header">
+      <div
+        :class="{ 'kefu-header-item-activation': tabActivation('会员信息') }"
+        class="kefu-header-item cursor-pointer flex items-center justify-center"
+        @click="handleClick('会员信息')"
+      >
+        会员信息
+      </div>
+      <div
+        :class="{ 'kefu-header-item-activation': tabActivation('最近浏览') }"
+        class="kefu-header-item cursor-pointer flex items-center justify-center"
+        @click="handleClick('最近浏览')"
+      >
+        最近浏览
+      </div>
+      <div
+        :class="{ 'kefu-header-item-activation': tabActivation('交易订单') }"
+        class="kefu-header-item cursor-pointer flex items-center justify-center"
+        @click="handleClick('交易订单')"
+      >
+        交易订单
+      </div>
+    </el-header>
+    <el-main class="kefu-content">
+      <div v-show="!isEmpty(conversation)">
+        <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
+          <!-- 最近浏览 -->
+          <ProductBrowsingHistory v-if="activeTab === '最近浏览'" ref="productBrowsingHistoryRef" />
+          <!-- 交易订单 -->
+          <OrderBrowsingHistory v-if="activeTab === '交易订单'" ref="orderBrowsingHistoryRef" />
+        </el-scrollbar>
+      </div>
+      <el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
+    </el-main>
+  </el-container>
+</template>
+
+<script lang="ts" setup>
+import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
+import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { isEmpty } from '@/utils/is'
+import { debounce } from 'lodash-es'
+import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index'
+
+defineOptions({ name: 'MemberBrowsingHistory' })
+
+const activeTab = ref('会员信息')
+const tabActivation = computed(() => (tab: string) => activeTab.value === tab)
+/** tab 切换 */
+const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
+const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
+const handleClick = async (tab: string) => {
+  activeTab.value = tab
+  await nextTick()
+  await getHistoryList()
+}
+
+/** 获得历史数据 */
+const getHistoryList = async () => {
+  switch (activeTab.value) {
+    case '会员信息':
+      break
+    case '最近浏览':
+      await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
+      break
+    case '交易订单':
+      await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
+      break
+    default:
+      break
+  }
+}
+
+/** 加载下一页数据 */
+const loadMore = async () => {
+  switch (activeTab.value) {
+    case '会员信息':
+      break
+    case '最近浏览':
+      await productBrowsingHistoryRef.value?.loadMore()
+      break
+    case '交易订单':
+      await orderBrowsingHistoryRef.value?.loadMore()
+      break
+    default:
+      break
+  }
+}
+
+/** 浏览历史初始化 */
+const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
+const initHistory = async (val: KeFuConversationRespVO) => {
+  activeTab.value = '会员信息'
+  conversation.value = val
+  await nextTick()
+  await getHistoryList()
+}
+defineExpose({ initHistory })
+
+/** 处理消息列表滚动事件(debounce 限流) */
+const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
+const handleScroll = debounce(() => {
+  const wrap = scrollbarRef.value?.wrapRef
+  // 触底重置
+  if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
+    loadMore()
+  }
+}, 200)
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+  width: 300px !important;
+  background-color: #fff;
+  border-left: var(--el-border-color) solid 1px;
+
+  &-header {
+    background: #fbfbfb;
+    box-shadow: 0 0 0 0 #dcdfe6;
+    display: flex;
+    align-items: center;
+    justify-content: space-around;
+
+    &-title {
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    &-item {
+      height: 100%;
+      width: 100%;
+      position: relative;
+
+      &-activation::before {
+        content: '';
+        position: absolute; /* 绝对定位 */
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0; /* 覆盖整个元素 */
+        border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
+        pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
+      }
+
+      &:hover::before {
+        content: '';
+        position: absolute; /* 绝对定位 */
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0; /* 覆盖整个元素 */
+        border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
+        pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
+      }
+    }
+  }
+
+  &-content {
+    margin: 0;
+    padding: 0;
+    position: relative;
+    height: 100%;
+    width: 100%;
+  }
+
+  &-tabs {
+    height: 100%;
+    width: 100%;
+  }
+}
+
+.header-title {
+  border-bottom: #e4e0e0 solid 1px;
+}
+</style>

+ 0 - 0
src/views/mall/promotion/kefu/components/history/OrderBrowsingHistory.vue → src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue


+ 0 - 0
src/views/mall/promotion/kefu/components/history/ProductBrowsingHistory.vue → src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue


+ 15 - 23
src/views/mall/promotion/kefu/index.vue

@@ -1,28 +1,16 @@
 <template>
-  <el-row :gutter="10">
+  <el-container class="kefu-layout">
     <!-- 会话列表 -->
-    <el-col :span="6">
-      <ContentWrap>
-        <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
-      </ContentWrap>
-    </el-col>
+    <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
     <!-- 会话详情(选中会话的消息列表) -->
-    <el-col :span="12">
-      <ContentWrap>
-        <KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
-      </ContentWrap>
-    </el-col>
+    <KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
     <!-- 会员足迹(选中会话的会员足迹) -->
-    <el-col :span="6">
-      <ContentWrap>
-        <MemberBrowsingHistory ref="memberBrowsingHistoryRef" />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+    <MemberInfo ref="memberInfoRef" />
+  </el-container>
 </template>
 
 <script lang="ts" setup>
-import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
+import { KeFuConversationList, KeFuMessageList, MemberInfo } from './components'
 import { WebSocketMessageTypeConstants } from './components/tools/constants'
 import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { getRefreshToken } from '@/utils/auth'
@@ -91,10 +79,10 @@ const getConversationList = () => {
 
 /** 加载指定会话的消息列表 */
 const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
-const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
+const memberInfoRef = ref<InstanceType<typeof MemberInfo>>()
 const handleChange = (conversation: KeFuConversationRespVO) => {
   keFuChatBoxRef.value?.getNewMessageList(conversation)
-  memberBrowsingHistoryRef.value?.initHistory(conversation)
+  memberInfoRef.value?.initHistory(conversation)
 }
 
 /** 初始化 */
@@ -112,9 +100,13 @@ onBeforeUnmount(() => {
 </script>
 
 <style lang="scss">
-.kefu {
-  height: calc(100vh - 165px);
-  overflow: auto; /* 确保内容可滚动 */
+.kefu-layout {
+  position: absolute;
+  flex: 1;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
 }
 
 /* 定义滚动条样式 */