Browse Source

!584 【功能完善】商城: 客服
Merge pull request !584 from puhui999/dev

芋道源码 9 months ago
parent
commit
18c7693ed6

+ 7 - 41
src/store/modules/mall/kefu.ts

@@ -63,48 +63,14 @@ export const useMallKefuStore = defineStore('mall-kefu', {
       }
     },
     conversationSort() {
-      // TODO @puhui999:1)逻辑上,先按照置顶、再按照最后消息时间;2)感觉写的有一丢丢小复杂,发给大模型,看看有没可能简化哈。
-      this.conversationList.sort((obj1, obj2) => {
-        // 如果 obj1.adminPinned 为 true,obj2.adminPinned 为 false,obj1 应该排在前面
-        if (obj1.adminPinned && !obj2.adminPinned) {
-          return -1
+      // 按置顶属性和最后消息时间排序
+      this.conversationList.sort((a, b) => {
+        // 按照置顶排序,置顶的会在前面
+        if (a.adminPinned !== b.adminPinned) {
+          return a.adminPinned ? -1 : 1
         }
-        // 如果 obj1.adminPinned 为 false,obj2.adminPinned 为 true,obj2 应该排在前面
-        if (!obj1.adminPinned && obj2.adminPinned) {
-          return 1
-        }
-
-        // 如果 obj1.adminPinned 和 obj2.adminPinned 都为 true,比较 adminUnreadMessageCount 的值
-        if (obj1.adminPinned && obj2.adminPinned) {
-          return obj1.adminUnreadMessageCount - obj2.adminUnreadMessageCount
-        }
-
-        // 如果 obj1.adminPinned 和 obj2.adminPinned 都为 false,比较 adminUnreadMessageCount 的值
-        if (!obj1.adminPinned && !obj2.adminPinned) {
-          return obj1.adminUnreadMessageCount - obj2.adminUnreadMessageCount
-        }
-
-        // 如果 obj1.adminPinned 为 true,obj2.adminPinned 为 true,且 b 都大于 0,比较 adminUnreadMessageCount 的值
-        if (
-          obj1.adminPinned &&
-          obj2.adminPinned &&
-          obj1.adminUnreadMessageCount > 0 &&
-          obj2.adminUnreadMessageCount > 0
-        ) {
-          return obj1.adminUnreadMessageCount - obj2.adminUnreadMessageCount
-        }
-
-        // 如果 obj1.adminPinned 为 false,obj2.adminPinned 为 false,且 b 都大于 0,比较 adminUnreadMessageCount 的值
-        if (
-          !obj1.adminPinned &&
-          !obj2.adminPinned &&
-          obj1.adminUnreadMessageCount > 0 &&
-          obj2.adminUnreadMessageCount > 0
-        ) {
-          return obj1.adminUnreadMessageCount - obj2.adminUnreadMessageCount
-        }
-
-        return 0
+        // 按照最后消息时间排序,最近的会在前面
+        return (b.lastMessageTime as unknown as number) - (a.lastMessageTime as unknown as number)
       })
     }
   }

+ 1 - 1
src/utils/index.ts

@@ -1,4 +1,4 @@
-import { toNumber } from 'lodash-es'
+import {toNumber} from 'lodash-es'
 
 /**
  *

+ 4 - 3
src/views/mall/promotion/kefu/components/KeFuConversationList.vue

@@ -1,7 +1,7 @@
 <template>
   <el-aside class="kefu p-5px h-100%" width="260px">
-    <div class="color-[#999] font-bold my-10px"
-      >会话记录({{ kefuStore.getConversationList.length }})
+    <div class="color-[#999] font-bold my-10px">
+      会话记录({{ kefuStore.getConversationList.length }})
     </div>
     <div
       v-for="item in kefuStore.getConversationList"
@@ -78,6 +78,7 @@ import { formatPast } from '@/utils/formatTime'
 import { KeFuMessageContentTypeEnum } from './tools/constants'
 import { useAppStore } from '@/store/modules/app'
 import { useMallKefuStore } from '@/store/modules/mall/kefu'
+import { jsonParse } from '@/utils'
 
 defineOptions({ name: 'KeFuConversationList' })
 
@@ -118,7 +119,7 @@ const getConversationDisplayText = computed(
       case KeFuMessageContentTypeEnum.VOICE:
         return '[语音消息]'
       case KeFuMessageContentTypeEnum.TEXT:
-        return replaceEmoji(lastMessageContent)
+        return replaceEmoji(jsonParse(lastMessageContent).text || lastMessageContent)
       default:
         return ''
     }

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

@@ -52,7 +52,7 @@
                 <MessageItem :message="item">
                   <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
                     <div
-                      v-dompurify-html="replaceEmoji(item.content)"
+                      v-dompurify-html="replaceEmoji(getMessageContent(item).text || item.content)"
                       class="flex items-center"
                     ></div>
                   </template>
@@ -62,8 +62,8 @@
                   <el-image
                     v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
                     :initial-index="0"
-                    :preview-src-list="[item.content]"
-                    :src="item.content"
+                    :preview-src-list="[getMessageContent(item).picUrl || item.content]"
+                    :src="getMessageContent(item).picUrl || item.content"
                     class="w-200px"
                     fit="contain"
                     preview-teleported
@@ -75,12 +75,11 @@
                     v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
                     :picUrl="getMessageContent(item).picUrl"
                     :price="getMessageContent(item).price"
-                    :skuText="getMessageContent(item).introduction"
+                    :sales-count="getMessageContent(item).salesCount"
                     :spuId="getMessageContent(item).spuId"
+                    :stock="getMessageContent(item).stock"
                     :title="getMessageContent(item).spuName"
-                    :titleWidth="400"
-                    class="max-w-70%"
-                    priceColor="#FF3000"
+                    class="max-w-300px"
                   />
                 </MessageItem>
                 <!-- 订单消息 -->
@@ -245,6 +244,7 @@ const getNewMessageList = async (val: KeFuConversationRespVO) => {
   total.value = messageList.value.length || 0
   loadHistory.value = false
   refreshContent.value = false
+  skipGetMessageList.value = false
   // 2.2 设置会话相关属性
   conversation.value = val
   queryParams.conversationId = val.id
@@ -268,7 +268,7 @@ const handleSendPicture = async (picUrl: string) => {
   const msg = {
     conversationId: conversation.value.id,
     contentType: KeFuMessageContentTypeEnum.IMAGE,
-    content: picUrl
+    content: JSON.stringify({ picUrl })
   }
   await sendMessage(msg)
 }
@@ -288,7 +288,7 @@ const handleSendMessage = async (event: any) => {
   const msg = {
     conversationId: conversation.value.id,
     contentType: KeFuMessageContentTypeEnum.TEXT,
-    content: message.value
+    content: JSON.stringify({ text: message.value })
   }
   await sendMessage(msg)
 }
@@ -392,7 +392,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
 
   &-content {
     margin: 0;
-    padding: 0;
+    padding: 10px;
     position: relative;
     height: 100%;
     width: 100%;

+ 55 - 1
src/views/mall/promotion/kefu/components/member/MemberInfo.vue

@@ -24,7 +24,22 @@
         交易订单
       </div>
     </el-header>
-    <el-main class="kefu-content">
+    <el-main class="kefu-content p-10px!">
+      <div v-if="!isEmpty(conversation)" v-loading="loading">
+        <!-- 基本信息 -->
+        <UserBasicInfo v-if="activeTab === '会员信息'" :user="user" mode="kefu">
+          <template #header>
+            <CardTitle title="基本信息" />
+          </template>
+        </UserBasicInfo>
+        <!-- 账户信息 -->
+        <el-card v-if="activeTab === '会员信息'" class="h-full mt-10px" shadow="never">
+          <template #header>
+            <CardTitle title="账户信息" />
+          </template>
+          <UserAccountInfo :column="1" :user="user" :wallet="wallet" />
+        </el-card>
+      </div>
       <div v-show="!isEmpty(conversation)">
         <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
           <!-- 最近浏览 -->
@@ -45,11 +60,17 @@ 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'
+import { CardTitle } from '@/components/Card'
+import UserBasicInfo from '@/views/member/user/detail/UserBasicInfo.vue'
+import UserAccountInfo from '@/views/member/user/detail/UserAccountInfo.vue'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
 
 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>>()
@@ -63,6 +84,8 @@ const handleClick = async (tab: string) => {
 const getHistoryList = async () => {
   switch (activeTab.value) {
     case '会员信息':
+      await getUserData()
+      await getUserWallet()
       break
     case '最近浏览':
       await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
@@ -110,6 +133,37 @@ const handleScroll = debounce(() => {
     loadMore()
   }
 }, 200)
+
+/* 用户钱包相关信息 */
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!conversation.value.userId) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: conversation.value.userId }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+const loading = ref(true) // 加载中
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+
+/** 获得用户 */
+const getUserData = async () => {
+  loading.value = true
+  try {
+    user.value = await UserApi.getUser(conversation.value.userId)
+  } finally {
+    loading.value = false
+  }
+}
 </script>
 
 <style lang="scss" scoped>

+ 3 - 4
src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue

@@ -1,15 +1,14 @@
 <template>
   <ProductItem
     v-for="item in list"
-    :spu-id="item.spuId"
     :key="item.id"
     :picUrl="item.picUrl"
     :price="item.price"
-    :skuText="item.introduction"
+    :sales-count="item.salesCount"
+    :spu-id="item.spuId"
+    :stock="item.stock"
     :title="item.spuName"
-    :titleWidth="400"
     class="mb-10px"
-    priceColor="#FF3000"
   />
 </template>
 

+ 10 - 7
src/views/mall/promotion/kefu/components/message/OrderItem.vue

@@ -14,11 +14,11 @@
       </div>
       <div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
         <ProductItem
-          :spu-id="item.spuId"
           :num="item.count"
           :picUrl="item.picUrl"
           :price="item.price"
           :skuText="item.properties.map((property: any) => property.valueName).join(' ')"
+          :spu-id="item.spuId"
           :title="item.spuName"
         />
       </div>
@@ -112,14 +112,14 @@ function formatOrderStatus(order: any) {
   border-radius: 10px;
   padding: 10px;
   border: 1px var(--el-border-color) solid;
-  background-color: var(--app-content-bg-color);
+  background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
 
   .order-card-header {
     height: 28px;
+    font-weight: bold;
 
     .order-no {
-      font-size: 12px;
-      font-weight: 500;
+      font-size: 13px;
 
       span {
         &:hover {
@@ -128,27 +128,30 @@ function formatOrderStatus(order: any) {
         }
       }
     }
+
+    .order-state {
+      font-size: 13px;
+    }
   }
 
   .pay-box {
     padding-top: 10px;
+    color: #fff;
+    font-weight: bold;
 
     .discounts-title {
       font-size: 16px;
       line-height: normal;
-      color: #999999;
     }
 
     .discounts-money {
       font-size: 16px;
       line-height: normal;
-      color: #999;
       font-family: OPPOSANS;
     }
 
     .pay-color {
       font-size: 13px;
-      color: var(--left-menu-text-color);
     }
   }
 }

+ 60 - 148
src/views/mall/promotion/kefu/components/message/ProductItem.vue

@@ -1,51 +1,27 @@
 <template>
-  <div @click.stop="openDetail(props.spuId)" style="cursor: pointer;">
-    <div>
-      <slot name="top"></slot>
+  <div class="product-warp" style="cursor: pointer" @click.stop="openDetail(spuId)">
+    <!-- 左侧商品图片-->
+    <div class="product-warp-left mr-24px">
+      <el-image
+        :initial-index="0"
+        :preview-src-list="[picUrl]"
+        :src="picUrl"
+        class="product-warp-left-img"
+        fit="contain"
+        preview-teleported
+        @click.stop
+      />
     </div>
-    <div
-      :style="[{ borderRadius: radius + 'px', marginBottom: marginBottom + 'px' }]"
-      class="ss-order-card-warp flex items-stretch justify-between bg-white"
-    >
-      <div class="img-box mr-24px">
-        <el-image
-          :initial-index="0"
-          :preview-src-list="[picUrl]"
-          :src="picUrl"
-          class="order-img"
-          fit="contain"
-          preview-teleported
-          @click.stop
-        />
+    <!-- 右侧商品信息 -->
+    <div class="product-warp-right">
+      <div class="description">{{ title }}</div>
+      <div class="my-5px">
+        <span class="mr-20px">库存: {{ stock || 0 }}</span>
+        <span>销量: {{ salesCount || 0 }}</span>
       </div>
-      <div
-        :style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
-        class="box-right flex flex-col justify-between"
-      >
-        <div v-if="title" class="title-text ss-line-2">{{ title }}</div>
-        <div v-if="skuString" class="spec-text mt-8px mb-12px">{{ skuString }}</div>
-        <div class="groupon-box">
-          <slot name="groupon"></slot>
-        </div>
-        <div class="flex">
-          <div class="flex items-center">
-            <div
-              v-if="price && Number(price) > 0"
-              :style="[{ color: priceColor }]"
-              class="price-text flex items-center"
-            >
-              ¥{{ fenToYuan(price) }}
-            </div>
-            <div v-if="num" class="total-text flex items-center">x {{ num }}</div>
-            <slot name="priceSuffix"></slot>
-          </div>
-        </div>
-        <div class="tool-box">
-          <slot name="tool"></slot>
-        </div>
-        <div>
-          <slot name="rightBottom"></slot>
-        </div>
+      <div class="flex justify-between items-center">
+        <span class="price">¥{{ fenToYuan(price) }}</span>
+        <el-button size="small" text type="primary">详情</el-button>
       </div>
     </div>
   </div>
@@ -57,7 +33,7 @@ import { fenToYuan } from '@/utils'
 const { push } = useRouter()
 
 defineOptions({ name: 'ProductItem' })
-const props = defineProps({
+defineProps({
   spuId: {
     type: Number,
     default: 0
@@ -70,134 +46,70 @@ const props = defineProps({
     type: String,
     default: ''
   },
-  titleWidth: {
-    type: Number,
-    default: 0
-  },
-  skuText: {
-    type: [String, Array],
-    default: ''
-  },
   price: {
     type: [String, Number],
     default: ''
   },
-  priceColor: {
-    type: [String],
-    default: ''
-  },
-  num: {
+  salesCount: {
     type: [String, Number],
-    default: 0
-  },
-  score: {
-    type: [String, Number],
-    default: ''
-  },
-  radius: {
-    type: [String],
     default: ''
   },
-  marginBottom: {
-    type: [String],
+  stock: {
+    type: [String, Number],
     default: ''
   }
 })
 
-/** SKU 展示字符串 */
-const skuString = computed(() => {
-  if (!props.skuText) {
-    return ''
-  }
-  if (typeof props.skuText === 'object') {
-    return props.skuText.join(',')
-  }
-  return props.skuText
-})
-
 /** 查看商品详情 */
 const openDetail = (spuId: number) => {
-  console.log(props.spuId)
   push({ name: 'ProductSpuDetail', params: { id: spuId } })
 }
 </script>
 
 <style lang="scss" scoped>
-.ss-order-card-warp {
-  padding: 20px;
-  border-radius: 10px;
-  border: 1px var(--el-border-color) solid;
-  background-color: var(--app-content-bg-color);
-
-  .img-box {
-    width: 80px;
-    height: 80px;
-    border-radius: 10px;
-    overflow: hidden;
+.button {
+  background-color: #007bff;
+  color: white;
+  border: none;
+  padding: 5px 10px;
+  cursor: pointer;
+}
 
-    .order-img {
-      width: 80px;
-      height: 80px;
+.product-warp {
+  width: 100%;
+  background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+
+  &-left {
+    width: 70px;
+
+    &-img {
+      width: 100%;
+      height: 100%;
+      border-radius: 8px;
     }
   }
 
-  .box-right {
+  &-right {
     flex: 1;
-    position: relative;
 
-    .tool-box {
-      position: absolute;
-      right: 0;
-      bottom: -10px;
+    .description {
+      width: 100%;
+      font-size: 16px;
+      font-weight: bold;
+      display: -webkit-box;
+      -webkit-line-clamp: 1; /* 显示一行 */
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+      text-overflow: ellipsis;
     }
-  }
-
-  .title-text {
-    font-size: 13px;
-    font-weight: 500;
-    line-height: 20px;
-  }
-
-  .spec-text {
-    font-size: 10px;
-    font-weight: 400;
-    color: #999999;
-    min-width: 0;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    display: -webkit-box;
-    -webkit-line-clamp: 1;
-    -webkit-box-orient: vertical;
-  }
-
-  .price-text {
-    font-size: 11px;
-    font-weight: 500;
-    font-family: OPPOSANS;
-  }
-
-  .total-text {
-    font-size: 10px;
-    font-weight: 400;
-    line-height: 16px;
-    color: #999999;
-    margin-left: 8px;
-  }
-}
 
-.ss-line {
-  min-width: 0;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-
-  &-1 {
-    -webkit-line-clamp: 1;
-  }
-
-  &-2 {
-    -webkit-line-clamp: 2;
+    .price {
+      color: #ff3000;
+    }
   }
 }
 </style>

+ 4 - 3
src/views/mall/promotion/kefu/index.vue

@@ -16,6 +16,7 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
 import { getRefreshToken } from '@/utils/auth'
 import { useWebSocket } from '@vueuse/core'
 import { useMallKefuStore } from '@/store/modules/mall/kefu'
+import { jsonParse } from '@/utils'
 
 defineOptions({ name: 'KeFu' })
 
@@ -30,6 +31,7 @@ const server = ref(
 ) // WebSocket 服务地址
 
 /** 发起 WebSocket 连接 */
+// TODO puhui999: websocket 连接有点问题收不到消息 🤣
 const { data, close, open } = useWebSocket(server.value, {
   autoReconnect: true,
   heartbeat: true
@@ -45,9 +47,9 @@ watchEffect(() => {
     if (data.value === 'pong') {
       return
     }
-
     // 2.1 解析 type 消息类型
     const jsonMessage = JSON.parse(data.value)
+    console.log(jsonMessage)
     const type = jsonMessage.type
     if (!type) {
       message.error('未知的消息类型:' + data.value)
@@ -57,7 +59,6 @@ watchEffect(() => {
     if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
       const message = JSON.parse(jsonMessage.content)
       // 刷新会话列表
-      // TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
       kefuStore.updateConversation(message.conversationId)
       // 刷新消息列表
       keFuChatBoxRef.value?.refreshMessageList(message)
@@ -66,7 +67,7 @@ watchEffect(() => {
     // 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
     if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
       // 更新会话已读
-      kefuStore.updateConversationStatus(JSON.parse(jsonMessage.content)?.id)
+      kefuStore.updateConversationStatus(jsonParse(jsonMessage.content))
     }
   } catch (error) {
     console.error(error)

+ 4 - 2
src/views/member/user/detail/UserAccountInfo.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-descriptions :column="2">
+  <el-descriptions :column="column">
     <el-descriptions-item>
       <template #label>
         <descriptions-item-label icon="svg-icon:member_level" label=" 等级 " />
@@ -50,7 +50,9 @@ import * as UserApi from '@/api/member/user'
 import * as WalletApi from '@/api/pay/wallet/balance'
 import { fenToYuan } from '@/utils'
 
-defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO }>() // 用户信息
+withDefaults(defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO; column?: number }>(), {
+  column: 2
+}) // 用户信息
 </script>
 <style lang="scss" scoped>
 .cell-item {

+ 78 - 17
src/views/member/user/detail/UserBasicInfo.vue

@@ -3,80 +3,141 @@
     <template #header>
       <slot name="header"></slot>
     </template>
-    <el-row>
+    <el-row v-if="mode === 'member'">
       <el-col :span="4">
-        <ElAvatar shape="square" :size="140" :src="user.avatar || undefined" />
+        <ElAvatar :size="140" :src="user.avatar || undefined" shape="square" />
       </el-col>
       <el-col :span="20">
         <el-descriptions :column="2">
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="用户名" icon="ep:user" />
+              <descriptions-item-label icon="ep:user" label="用户名" />
             </template>
             {{ user.name || '空' }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="昵称" icon="ep:user" />
+              <descriptions-item-label icon="ep:user" label="昵称" />
             </template>
             {{ user.nickname }}
           </el-descriptions-item>
           <el-descriptions-item label="手机号">
             <template #label>
-              <descriptions-item-label label="手机号" icon="ep:phone" />
+              <descriptions-item-label icon="ep:phone" label="手机号" />
             </template>
             {{ user.mobile }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="性别" icon="fa:mars-double" />
+              <descriptions-item-label icon="fa:mars-double" label="性别" />
             </template>
             <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="所在地" icon="ep:location" />
+              <descriptions-item-label icon="ep:location" label="所在地" />
             </template>
             {{ user.areaName }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="注册 IP" icon="ep:position" />
+              <descriptions-item-label icon="ep:position" label="注册 IP" />
             </template>
             {{ user.registerIp }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="生日" icon="fa:birthday-cake" />
+              <descriptions-item-label icon="fa:birthday-cake" label="生日" />
             </template>
-            {{ user.birthday ? formatDate(user.birthday) : '空' }}
+            {{ user.birthday ? formatDate(user.birthday as any) : '空' }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="注册时间" icon="ep:calendar" />
+              <descriptions-item-label icon="ep:calendar" label="注册时间" />
             </template>
-            {{ user.createTime ? formatDate(user.createTime) : '空' }}
+            {{ user.createTime ? formatDate(user.createTime as any) : '空' }}
           </el-descriptions-item>
           <el-descriptions-item>
             <template #label>
-              <descriptions-item-label label="最后登录时间" icon="ep:calendar" />
+              <descriptions-item-label icon="ep:calendar" label="最后登录时间" />
             </template>
-            {{ user.loginDate ? formatDate(user.loginDate) : '空' }}
+            {{ user.loginDate ? formatDate(user.loginDate as any) : '空' }}
           </el-descriptions-item>
         </el-descriptions>
       </el-col>
     </el-row>
+    <template v-if="mode === 'kefu'">
+      <ElAvatar :size="140" :src="user.avatar || undefined" shape="square" />
+      <el-descriptions :column="1">
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:user" label="用户名" />
+          </template>
+          {{ user.name || '空' }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:user" label="昵称" />
+          </template>
+          {{ user.nickname }}
+        </el-descriptions-item>
+        <el-descriptions-item label="手机号">
+          <template #label>
+            <descriptions-item-label icon="ep:phone" label="手机号" />
+          </template>
+          {{ user.mobile }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="fa:mars-double" label="性别" />
+          </template>
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:location" label="所在地" />
+          </template>
+          {{ user.areaName }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:position" label="注册 IP" />
+          </template>
+          {{ user.registerIp }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="fa:birthday-cake" label="生日" />
+          </template>
+          {{ user.birthday ? formatDate(user.birthday as any) : '空' }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:calendar" label="注册时间" />
+          </template>
+          {{ user.createTime ? formatDate(user.createTime as any) : '空' }}
+        </el-descriptions-item>
+        <el-descriptions-item>
+          <template #label>
+            <descriptions-item-label icon="ep:calendar" label="最后登录时间" />
+          </template>
+          {{ user.loginDate ? formatDate(user.loginDate as any) : '空' }}
+        </el-descriptions-item>
+      </el-descriptions>
+    </template>
   </el-card>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
 import { DICT_TYPE } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
 import * as UserApi from '@/api/member/user'
 import { DescriptionsItemLabel } from '@/components/Descriptions/index'
 
-const { user } = defineProps<{ user: UserApi.UserVO }>()
+withDefaults(defineProps<{ user: UserApi.UserVO; mode?: string }>(), {
+  mode: 'member'
+})
 </script>
-<style scoped lang="scss">
+<style lang="scss" scoped>
 .card-header {
   display: flex;
   justify-content: space-between;