commit
18c7693ed6
|
@ -63,48 +63,14 @@ export const useMallKefuStore = defineStore('mall-kefu', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
conversationSort() {
|
conversationSort() {
|
||||||
// TODO @puhui999:1)逻辑上,先按照置顶、再按照最后消息时间;2)感觉写的有一丢丢小复杂,发给大模型,看看有没可能简化哈。
|
// 按置顶属性和最后消息时间排序
|
||||||
this.conversationList.sort((obj1, obj2) => {
|
this.conversationList.sort((a, b) => {
|
||||||
// 如果 obj1.adminPinned 为 true,obj2.adminPinned 为 false,obj1 应该排在前面
|
// 按照置顶排序,置顶的会在前面
|
||||||
if (obj1.adminPinned && !obj2.adminPinned) {
|
if (a.adminPinned !== b.adminPinned) {
|
||||||
return -1
|
return a.adminPinned ? -1 : 1
|
||||||
}
|
}
|
||||||
// 如果 obj1.adminPinned 为 false,obj2.adminPinned 为 true,obj2 应该排在前面
|
// 按照最后消息时间排序,最近的会在前面
|
||||||
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 为 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
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ''
|
||||||
}
|
}
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
</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
|
<el-image
|
||||||
:initial-index="0"
|
:initial-index="0"
|
||||||
:preview-src-list="[picUrl]"
|
:preview-src-list="[picUrl]"
|
||||||
:src="picUrl"
|
:src="picUrl"
|
||||||
class="order-img"
|
class="product-warp-left-img"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
preview-teleported
|
preview-teleported
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<!-- 右侧商品信息 -->
|
||||||
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
|
<div class="product-warp-right">
|
||||||
class="box-right flex flex-col justify-between"
|
<div class="description">{{ title }}</div>
|
||||||
>
|
<div class="my-5px">
|
||||||
<div v-if="title" class="title-text ss-line-2">{{ title }}</div>
|
<span class="mr-20px">库存: {{ stock || 0 }}</span>
|
||||||
<div v-if="skuString" class="spec-text mt-8px mb-12px">{{ skuString }}</div>
|
<span>销量: {{ salesCount || 0 }}</span>
|
||||||
<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 class="flex justify-between items-center">
|
||||||
|
<span class="price">¥{{ fenToYuan(price) }}</span>
|
||||||
|
<el-button size="small" text type="primary">详情</el-button>
|
||||||
</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 {
|
.product-warp {
|
||||||
width: 80px;
|
width: 100%;
|
||||||
height: 80px;
|
background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
.order-img {
|
&-left {
|
||||||
width: 80px;
|
width: 70px;
|
||||||
height: 80px;
|
|
||||||
|
&-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.box-right {
|
&-right {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.tool-box {
|
.description {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
right: 0;
|
font-size: 16px;
|
||||||
bottom: -10px;
|
font-weight: bold;
|
||||||
}
|
display: -webkit-box;
|
||||||
}
|
-webkit-line-clamp: 1; /* 显示一行 */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
.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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-text {
|
.price {
|
||||||
font-size: 11px;
|
color: #ff3000;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue