!584 【功能完善】商城: 客服

Merge pull request !584 from puhui999/dev
pull/589/head
芋道源码 2024-11-10 10:41:54 +00:00 committed by Gitee
commit 18c7693ed6
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
11 changed files with 239 additions and 240 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,22 @@
交易订单 交易订单
</div> </div>
</el-header> </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)"> <div v-show="!isEmpty(conversation)">
<el-scrollbar ref="scrollbarRef" always @scroll="handleScroll"> <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 { isEmpty } from '@/utils/is'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index' 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' }) defineOptions({ name: 'MemberBrowsingHistory' })
const activeTab = ref('会员信息') const activeTab = ref('会员信息')
const tabActivation = computed(() => (tab: string) => activeTab.value === tab) const tabActivation = computed(() => (tab: string) => activeTab.value === tab)
/** tab 切换 */ /** tab 切换 */
const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>() const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>() const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
@ -63,6 +84,8 @@ const handleClick = async (tab: string) => {
const getHistoryList = async () => { const getHistoryList = async () => {
switch (activeTab.value) { switch (activeTab.value) {
case '会员信息': case '会员信息':
await getUserData()
await getUserWallet()
break break
case '最近浏览': case '最近浏览':
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value) await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
@ -110,6 +133,37 @@ const handleScroll = debounce(() => {
loadMore() loadMore()
} }
}, 200) }, 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

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

View File

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

View File

@ -1,51 +1,27 @@
<template> <template>
<div @click.stop="openDetail(props.spuId)" style="cursor: pointer;"> <div class="product-warp" style="cursor: pointer" @click.stop="openDetail(spuId)">
<div> <!-- 左侧商品图片-->
<slot name="top"></slot> <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>
<div <!-- 右侧商品信息 -->
:style="[{ borderRadius: radius + 'px', marginBottom: marginBottom + 'px' }]" <div class="product-warp-right">
class="ss-order-card-warp flex items-stretch justify-between bg-white" <div class="description">{{ title }}</div>
> <div class="my-5px">
<div class="img-box mr-24px"> <span class="mr-20px">库存: {{ stock || 0 }}</span>
<el-image <span>销量: {{ salesCount || 0 }}</span>
:initial-index="0"
:preview-src-list="[picUrl]"
:src="picUrl"
class="order-img"
fit="contain"
preview-teleported
@click.stop
/>
</div> </div>
<div <div class="flex justify-between items-center">
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]" <span class="price">{{ fenToYuan(price) }}</span>
class="box-right flex flex-col justify-between" <el-button size="small" text type="primary">详情</el-button>
>
<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> </div>
</div> </div>
</div> </div>
@ -57,7 +33,7 @@ import { fenToYuan } from '@/utils'
const { push } = useRouter() const { push } = useRouter()
defineOptions({ name: 'ProductItem' }) defineOptions({ name: 'ProductItem' })
const props = defineProps({ defineProps({
spuId: { spuId: {
type: Number, type: Number,
default: 0 default: 0
@ -70,134 +46,70 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
titleWidth: {
type: Number,
default: 0
},
skuText: {
type: [String, Array],
default: ''
},
price: { price: {
type: [String, Number], type: [String, Number],
default: '' default: ''
}, },
priceColor: { salesCount: {
type: [String],
default: ''
},
num: {
type: [String, Number],
default: 0
},
score: {
type: [String, Number], type: [String, Number],
default: '' default: ''
}, },
radius: { stock: {
type: [String], type: [String, Number],
default: ''
},
marginBottom: {
type: [String],
default: '' 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) => { const openDetail = (spuId: number) => {
console.log(props.spuId)
push({ name: 'ProductSpuDetail', params: { id: spuId } }) push({ name: 'ProductSpuDetail', params: { id: spuId } })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.ss-order-card-warp { .button {
padding: 20px; background-color: #007bff;
border-radius: 10px; color: white;
border: 1px var(--el-border-color) solid; border: none;
background-color: var(--app-content-bg-color); padding: 5px 10px;
cursor: pointer;
.img-box {
width: 80px;
height: 80px;
border-radius: 10px;
overflow: hidden;
.order-img {
width: 80px;
height: 80px;
}
}
.box-right {
flex: 1;
position: relative;
.tool-box {
position: absolute;
right: 0;
bottom: -10px;
}
}
.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 { .product-warp {
min-width: 0; width: 100%;
overflow: hidden; background-color: rgba(128, 128, 128, 0.5); //
text-overflow: ellipsis; border-radius: 8px;
display: -webkit-box; display: flex;
-webkit-box-orient: vertical; align-items: center;
padding: 10px;
&-1 { &-left {
-webkit-line-clamp: 1; width: 70px;
&-img {
width: 100%;
height: 100%;
border-radius: 8px;
}
} }
&-2 { &-right {
-webkit-line-clamp: 2; flex: 1;
.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;
}
.price {
color: #ff3000;
}
} }
} }
</style> </style>

View File

@ -16,6 +16,7 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getRefreshToken } from '@/utils/auth' import { getRefreshToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core' import { useWebSocket } from '@vueuse/core'
import { useMallKefuStore } from '@/store/modules/mall/kefu' import { useMallKefuStore } from '@/store/modules/mall/kefu'
import { jsonParse } from '@/utils'
defineOptions({ name: 'KeFu' }) defineOptions({ name: 'KeFu' })
@ -30,6 +31,7 @@ const server = ref(
) // WebSocket ) // WebSocket
/** 发起 WebSocket 连接 */ /** 发起 WebSocket 连接 */
// TODO puhui999: websocket 🤣
const { data, close, open } = useWebSocket(server.value, { const { data, close, open } = useWebSocket(server.value, {
autoReconnect: true, autoReconnect: true,
heartbeat: true heartbeat: true
@ -45,9 +47,9 @@ watchEffect(() => {
if (data.value === 'pong') { if (data.value === 'pong') {
return return
} }
// 2.1 type // 2.1 type
const jsonMessage = JSON.parse(data.value) const jsonMessage = JSON.parse(data.value)
console.log(jsonMessage)
const type = jsonMessage.type const type = jsonMessage.type
if (!type) { if (!type) {
message.error('未知的消息类型:' + data.value) message.error('未知的消息类型:' + data.value)
@ -57,7 +59,6 @@ watchEffect(() => {
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) { if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
const message = JSON.parse(jsonMessage.content) const message = JSON.parse(jsonMessage.content)
// //
// TODO @puhui999 update
kefuStore.updateConversation(message.conversationId) kefuStore.updateConversation(message.conversationId)
// //
keFuChatBoxRef.value?.refreshMessageList(message) keFuChatBoxRef.value?.refreshMessageList(message)
@ -66,7 +67,7 @@ watchEffect(() => {
// 2.3 KEFU_MESSAGE_ADMIN_READ // 2.3 KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.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) { } catch (error) {
console.error(error) console.error(error)

View File

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

View File

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