Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm
commit
7c391a315e
10
package.json
10
package.json
|
@ -9,11 +9,11 @@
|
||||||
"dev": "vite --mode env.local",
|
"dev": "vite --mode env.local",
|
||||||
"dev-server": "vite --mode dev",
|
"dev-server": "vite --mode dev",
|
||||||
"ts:check": "vue-tsc --noEmit",
|
"ts:check": "vue-tsc --noEmit",
|
||||||
"build:local": "node ./node_modules/vite/bin/vite.js build",
|
"build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
|
||||||
"build:dev": "node ./node_modules/vite/bin/vite.js build --mode dev",
|
"build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev",
|
||||||
"build:test": "node ./node_modules/vite/bin/vite.js build --mode test",
|
"build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test",
|
||||||
"build:stage": "node ./node_modules/vite/bin/vite.js build --mode stage",
|
"build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage",
|
||||||
"build:prod": "node ./node_modules/vite/bin/vite.js build --mode prod",
|
"build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod",
|
||||||
"serve:dev": "vite preview --mode dev",
|
"serve:dev": "vite preview --mode dev",
|
||||||
"serve:prod": "vite preview --mode prod",
|
"serve:prod": "vite preview --mode prod",
|
||||||
"preview": "pnpm build:local && vite preview",
|
"preview": "pnpm build:local && vite preview",
|
||||||
|
|
|
@ -21,6 +21,10 @@ export const KeFuConversationApi = {
|
||||||
getConversationList: async () => {
|
getConversationList: async () => {
|
||||||
return await request.get({ url: '/promotion/kefu-conversation/list' })
|
return await request.get({ url: '/promotion/kefu-conversation/list' })
|
||||||
},
|
},
|
||||||
|
// 获得客服会话
|
||||||
|
getConversation: async (id: number) => {
|
||||||
|
return await request.get({ url: `/promotion/kefu-conversation/get?id=` + id })
|
||||||
|
},
|
||||||
// 客服会话置顶
|
// 客服会话置顶
|
||||||
updateConversationPinned: async (data: any) => {
|
updateConversationPinned: async (data: any) => {
|
||||||
return await request.put({
|
return await request.put({
|
||||||
|
@ -30,6 +34,6 @@ export const KeFuConversationApi = {
|
||||||
},
|
},
|
||||||
// 删除客服会话
|
// 删除客服会话
|
||||||
deleteConversation: async (id: number) => {
|
deleteConversation: async (id: number) => {
|
||||||
return await request.delete({ url: `/promotion/kefu-conversation/delete?id=${id}`})
|
return await request.delete({ url: `/promotion/kefu-conversation/delete?id=${id}` })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ export const KeFuMessageApi = {
|
||||||
url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
|
url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 获得消息分页数据
|
// 获得消息列表(流式加载)
|
||||||
getKeFuMessagePage: async (params: any) => {
|
getKeFuMessageList: async (params: any) => {
|
||||||
return await request.get({ url: '/promotion/kefu-message/page', params })
|
return await request.get({ url: '/promotion/kefu-message/list', params })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
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'
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
|
||||||
|
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(): (conversationId: number) => KeFuMessageRespVO[] | undefined {
|
||||||
|
return (conversationId: number) => this.conversationMessageList.get(conversationId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
// ======================= 会话消息相关 =======================
|
||||||
|
/** 缓存历史消息 */
|
||||||
|
saveMessageList(conversationId: number, messageList: KeFuMessageRespVO[]) {
|
||||||
|
this.conversationMessageList.set(conversationId, messageList)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ======================= 会话相关 =======================
|
||||||
|
/** 加载会话缓存列表 */
|
||||||
|
async setConversationList() {
|
||||||
|
this.conversationList = await KeFuConversationApi.getConversationList()
|
||||||
|
this.conversationSort()
|
||||||
|
},
|
||||||
|
/** 更新会话缓存已读 */
|
||||||
|
async updateConversationStatus(conversationId: number) {
|
||||||
|
if (isEmpty(this.conversationList)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const conversation = this.conversationList.find((item) => item.id === conversationId)
|
||||||
|
conversation && (conversation.adminUnreadMessageCount = 0)
|
||||||
|
},
|
||||||
|
/** 更新会话缓存 */
|
||||||
|
async updateConversation(conversationId: number) {
|
||||||
|
if (isEmpty(this.conversationList)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = await KeFuConversationApi.getConversation(conversationId)
|
||||||
|
this.deleteConversation(conversationId)
|
||||||
|
conversation && this.conversationList.push(conversation)
|
||||||
|
this.conversationSort()
|
||||||
|
},
|
||||||
|
/** 删除会话缓存 */
|
||||||
|
deleteConversation(conversationId: number) {
|
||||||
|
const index = this.conversationList.findIndex((item) => item.id === conversationId)
|
||||||
|
// 存在则删除
|
||||||
|
if (index > -1) {
|
||||||
|
this.conversationList.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conversationSort() {
|
||||||
|
// 按置顶属性和最后消息时间排序
|
||||||
|
this.conversationList.sort((a, b) => {
|
||||||
|
// 按照置顶排序,置顶的会在前面
|
||||||
|
if (a.adminPinned !== b.adminPinned) {
|
||||||
|
return a.adminPinned ? -1 : 1
|
||||||
|
}
|
||||||
|
// 按照最后消息时间排序,最近的会在前面
|
||||||
|
return (b.lastMessageTime as unknown as number) - (a.lastMessageTime as unknown as number)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useMallKefuStoreWithOut = () => {
|
||||||
|
return useMallKefuStore(store)
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="120px"
|
label-width="120px"
|
||||||
>
|
>
|
||||||
<el-form-item label="分销类型" props="subCommissionType">
|
<el-form-item label="分销类型" prop="subCommissionType">
|
||||||
<el-radio-group
|
<el-radio-group
|
||||||
v-model="formData.subCommissionType"
|
v-model="formData.subCommissionType"
|
||||||
class="w-80"
|
class="w-80"
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<el-radio :value="true" class="radio">单独设置</el-radio>
|
<el-radio :value="true" class="radio">单独设置</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="商品规格" props="specType">
|
<el-form-item label="商品规格" prop="specType">
|
||||||
<el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
|
<el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
|
||||||
<el-radio :value="false" class="radio">单规格</el-radio>
|
<el-radio :value="false" class="radio">单规格</el-radio>
|
||||||
<el-radio :value="true">多规格</el-radio>
|
<el-radio :value="true">多规格</el-radio>
|
||||||
|
|
|
@ -115,7 +115,7 @@ import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/produc
|
||||||
import { ElTable } from 'element-plus'
|
import { ElTable } from 'element-plus'
|
||||||
import { dateFormatter } from '@/utils/formatTime'
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
import { createImageViewer } from '@/components/ImageViewer'
|
import { createImageViewer } from '@/components/ImageViewer'
|
||||||
import { formatToFraction } from '@/utils'
|
import { floatToFixed2, formatToFraction } from '@/utils'
|
||||||
import { defaultProps, handleTree } from '@/utils/tree'
|
import { defaultProps, handleTree } from '@/utils/tree'
|
||||||
|
|
||||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||||
|
@ -228,6 +228,13 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi
|
||||||
}
|
}
|
||||||
// 获取 SPU 详情
|
// 获取 SPU 详情
|
||||||
const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
|
const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
|
||||||
|
res.skus?.forEach((item) => {
|
||||||
|
item.price = floatToFixed2(item.price)
|
||||||
|
item.marketPrice = floatToFixed2(item.marketPrice)
|
||||||
|
item.costPrice = floatToFixed2(item.costPrice)
|
||||||
|
item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
|
||||||
|
item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
|
||||||
|
})
|
||||||
propertyList.value = getPropertyList(res)
|
propertyList.value = getPropertyList(res)
|
||||||
spuData.value = res
|
spuData.value = res
|
||||||
isExpand.value = true
|
isExpand.value = true
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="kefu">
|
<el-aside class="kefu pt-5px h-100%" width="260px">
|
||||||
|
<div class="color-[#999] font-bold my-10px">
|
||||||
|
会话记录({{ kefuStore.getConversationList.length }})
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="item in conversationList"
|
v-for="item in kefuStore.getConversationList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
|
:class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
|
||||||
class="kefu-conversation flex items-center"
|
class="kefu-conversation px-10px flex items-center"
|
||||||
@click="openRightMessage(item)"
|
@click="openRightMessage(item)"
|
||||||
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
|
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
|
||||||
>
|
>
|
||||||
|
@ -22,8 +25,8 @@
|
||||||
<div class="ml-10px w-100%">
|
<div class="ml-10px w-100%">
|
||||||
<div class="flex justify-between items-center w-100%">
|
<div class="flex justify-between items-center w-100%">
|
||||||
<span class="username">{{ item.userNickname }}</span>
|
<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') }}
|
{{ lastMessageTimeMap.get(item.id) ?? '计算中' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 最后聊天内容 -->
|
<!-- 最后聊天内容 -->
|
||||||
|
@ -31,7 +34,7 @@
|
||||||
v-dompurify-html="
|
v-dompurify-html="
|
||||||
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,7 +68,7 @@
|
||||||
取消
|
取消
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</el-aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -74,29 +77,36 @@ import { useEmoji } from './tools/emoji'
|
||||||
import { formatPast } from '@/utils/formatTime'
|
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 { jsonParse } from '@/utils'
|
||||||
|
|
||||||
defineOptions({ name: 'KeFuConversationList' })
|
defineOptions({ name: 'KeFuConversationList' })
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const kefuStore = useMallKefuStore() // 客服缓存
|
||||||
const { replaceEmoji } = useEmoji()
|
const { replaceEmoji } = useEmoji()
|
||||||
const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
|
|
||||||
const activeConversationId = ref(-1) // 选中的会话
|
const activeConversationId = ref(-1) // 选中的会话
|
||||||
const collapse = computed(() => appStore.getCollapse) // 折叠菜单
|
const collapse = computed(() => appStore.getCollapse) // 折叠菜单
|
||||||
|
|
||||||
/** 加载会话列表 */
|
/** 计算消息最后发送时间距离现在过去了多久 */
|
||||||
const getConversationList = async () => {
|
const lastMessageTimeMap = ref<Map<number, string>>(new Map<number, string>())
|
||||||
const list = await KeFuConversationApi.getConversationList()
|
const calculationLastMessageTime = () => {
|
||||||
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
|
kefuStore.getConversationList?.forEach((item) => {
|
||||||
conversationList.value = list
|
lastMessageTimeMap.value.set(item.id, formatPast(item.lastMessageTime, 'YYYY-MM-DD'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
defineExpose({ getConversationList })
|
defineExpose({ calculationLastMessageTime })
|
||||||
|
|
||||||
/** 打开右侧的消息列表 */
|
/** 打开右侧的消息列表 */
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: 'change', v: KeFuConversationRespVO): void
|
(e: 'change', v: KeFuConversationRespVO): void
|
||||||
}>()
|
}>()
|
||||||
const openRightMessage = (item: KeFuConversationRespVO) => {
|
const openRightMessage = (item: KeFuConversationRespVO) => {
|
||||||
|
// 同一个会话则不处理
|
||||||
|
if (activeConversationId.value === item.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
activeConversationId.value = item.id
|
activeConversationId.value = item.id
|
||||||
emits('change', item)
|
emits('change', item)
|
||||||
}
|
}
|
||||||
|
@ -118,7 +128,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 ''
|
||||||
}
|
}
|
||||||
|
@ -155,7 +165,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
|
||||||
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
|
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
|
||||||
// 2. 关闭右键菜单,更新会话列表
|
// 2. 关闭右键菜单,更新会话列表
|
||||||
closeRightMenu()
|
closeRightMenu()
|
||||||
await getConversationList()
|
await kefuStore.updateConversation(rightClickConversation.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除会话 */
|
/** 删除会话 */
|
||||||
|
@ -165,7 +175,7 @@ const deleteConversation = async () => {
|
||||||
await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
|
await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
|
||||||
// 2. 关闭右键菜单,更新会话列表
|
// 2. 关闭右键菜单,更新会话列表
|
||||||
closeRightMenu()
|
closeRightMenu()
|
||||||
await getConversationList()
|
kefuStore.deleteConversation(rightClickConversation.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 监听右键菜单的显示状态,添加点击事件监听器 */
|
/** 监听右键菜单的显示状态,添加点击事件监听器 */
|
||||||
|
@ -176,42 +186,48 @@ watch(showRightMenu, (val) => {
|
||||||
document.body.removeEventListener('click', closeRightMenu)
|
document.body.removeEventListener('click', closeRightMenu)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const timer = ref<any>()
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
timer.value = setInterval(calculationLastMessageTime, 1000 * 10) // 十秒计算一次
|
||||||
|
})
|
||||||
|
/** 组件卸载前 */
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.kefu {
|
.kefu {
|
||||||
|
background-color: #e5e4e4;
|
||||||
|
|
||||||
&-conversation {
|
&-conversation {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 10px;
|
|
||||||
//background-color: #fff;
|
//background-color: #fff;
|
||||||
transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
|
//transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message,
|
||||||
|
.username {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last-message {
|
|
||||||
font-size: 13px;
|
|
||||||
width: 200px;
|
|
||||||
overflow: hidden; // 隐藏超出的文本
|
|
||||||
white-space: nowrap; // 禁止换行
|
|
||||||
text-overflow: ellipsis; // 添加省略号
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
border-left: 5px #3271ff solid;
|
background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
|
||||||
background-color: var(--login-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinned {
|
|
||||||
background-color: var(--left-menu-bg-active-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-menu-ul {
|
.right-menu-ul {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<el-container v-if="showKeFuMessageList" class="kefu">
|
<el-container v-if="showKeFuMessageList" class="kefu">
|
||||||
<el-header>
|
<el-header class="kefu-header">
|
||||||
<div class="kefu-title">{{ conversation.userNickname }}</div>
|
<div class="kefu-title">{{ conversation.userNickname }}</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main class="kefu-content overflow-visible">
|
<el-main class="kefu-content overflow-visible">
|
||||||
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
|
<el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
|
||||||
<div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
|
<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 v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
|
||||||
<div class="flex justify-center items-center mb-20px">
|
<div class="flex justify-center items-center mb-20px">
|
||||||
|
@ -43,15 +43,16 @@
|
||||||
class="w-60px h-60px"
|
class="w-60px h-60px"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
|
:class="{
|
||||||
class="p-10px"
|
'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<!-- 文本消息 -->
|
<!-- 文本消息 -->
|
||||||
<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="line-height-normal text-justify h-1/1 w-full"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
</MessageItem>
|
</MessageItem>
|
||||||
|
@ -60,9 +61,9 @@
|
||||||
<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 mx-10px"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
preview-teleported
|
preview-teleported
|
||||||
/>
|
/>
|
||||||
|
@ -71,14 +72,13 @@
|
||||||
<MessageItem :message="item">
|
<MessageItem :message="item">
|
||||||
<ProductItem
|
<ProductItem
|
||||||
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
|
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
|
||||||
:spuId="getMessageContent(item).spuId"
|
|
||||||
: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"
|
||||||
|
:stock="getMessageContent(item).stock"
|
||||||
:title="getMessageContent(item).spuName"
|
:title="getMessageContent(item).spuName"
|
||||||
:titleWidth="400"
|
class="max-w-300px mx-10px"
|
||||||
class="max-w-70%"
|
|
||||||
priceColor="#FF3000"
|
|
||||||
/>
|
/>
|
||||||
</MessageItem>
|
</MessageItem>
|
||||||
<!-- 订单消息 -->
|
<!-- 订单消息 -->
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
<OrderItem
|
<OrderItem
|
||||||
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
|
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
|
||||||
:message="item"
|
:message="item"
|
||||||
class="max-w-100%"
|
class="max-w-100% mx-10px"
|
||||||
/>
|
/>
|
||||||
</MessageItem>
|
</MessageItem>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,23 +108,29 @@
|
||||||
<Icon class="ml-5px" icon="ep:bottom" />
|
<Icon class="ml-5px" icon="ep:bottom" />
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer height="230px">
|
<el-footer class="kefu-footer">
|
||||||
<div class="h-[100%]">
|
<div class="chat-tools flex items-center">
|
||||||
<div class="chat-tools flex items-center">
|
<EmojiSelectPopover @select-emoji="handleEmojiSelect" />
|
||||||
<EmojiSelectPopover @select-emoji="handleEmojiSelect" />
|
<PictureSelectUpload
|
||||||
<PictureSelectUpload
|
class="ml-15px mt-3px cursor-pointer"
|
||||||
class="ml-15px mt-3px cursor-pointer"
|
@send-picture="handleSendPicture"
|
||||||
@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>
|
|
||||||
</div>
|
</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-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
<el-empty v-else description="请选择左侧的一个会话后开始" />
|
<el-container v-else class="kefu">
|
||||||
|
<el-main>
|
||||||
|
<el-empty description="请选择左侧的一个会话后开始" />
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -144,6 +150,7 @@ import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
import { jsonParse } from '@/utils'
|
import { jsonParse } from '@/utils'
|
||||||
|
import { useMallKefuStore } from '@/store/modules/mall/kefu'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
@ -156,25 +163,31 @@ const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
|
||||||
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
|
||||||
const showNewMessageTip = ref(false) // 显示有新消息提示
|
const showNewMessageTip = ref(false) // 显示有新消息提示
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
conversationId: 0,
|
||||||
pageSize: 10,
|
createTime: undefined
|
||||||
conversationId: 0
|
|
||||||
})
|
})
|
||||||
const total = ref(0) // 消息总条数
|
const total = ref(0) // 消息总条数
|
||||||
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
|
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
|
||||||
|
const kefuStore = useMallKefuStore() // 客服缓存
|
||||||
|
|
||||||
/** 获悉消息内容 */
|
/** 获悉消息内容 */
|
||||||
const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
|
const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
|
||||||
/** 获得消息列表 */
|
/** 获得消息列表 */
|
||||||
const getMessageList = async () => {
|
const getMessageList = async () => {
|
||||||
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
|
const res = await KeFuMessageApi.getKeFuMessageList(queryParams)
|
||||||
total.value = res.total
|
if (isEmpty(res)) {
|
||||||
|
// 当返回的是空列表说明没有消息或者已经查询完了历史消息
|
||||||
|
skipGetMessageList.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryParams.createTime = formatDate(res.at(-1).createTime) as any
|
||||||
|
|
||||||
// 情况一:加载最新消息
|
// 情况一:加载最新消息
|
||||||
if (queryParams.pageNo === 1) {
|
if (!queryParams.createTime) {
|
||||||
messageList.value = res.list
|
messageList.value = res
|
||||||
} else {
|
} else {
|
||||||
// 情况二:加载历史消息
|
// 情况二:加载历史消息
|
||||||
for (const item of res.list) {
|
for (const item of res) {
|
||||||
pushMessage(item)
|
pushMessage(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,8 +221,7 @@ const refreshMessageList = async (message?: any) => {
|
||||||
}
|
}
|
||||||
pushMessage(message)
|
pushMessage(message)
|
||||||
} else {
|
} else {
|
||||||
// TODO @puhui999:不基于 page 做。而是流式分页;通过 createTime 排序查询;
|
queryParams.createTime = undefined
|
||||||
queryParams.pageNo = 1
|
|
||||||
await getMessageList()
|
await getMessageList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,28 +234,27 @@ const refreshMessageList = async (message?: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获得新会话的消息列表 */
|
/** 获得新会话的消息列表, 点击切换时,读取缓存;然后异步获取新消息,merge 下; */
|
||||||
// TODO @puhui999:可优化:可以考虑本地做每个会话的消息 list 缓存;然后点击切换时,读取缓存;然后异步获取新消息,merge 下;
|
|
||||||
const getNewMessageList = async (val: KeFuConversationRespVO) => {
|
const getNewMessageList = async (val: KeFuConversationRespVO) => {
|
||||||
// 会话切换,重置相关参数
|
// 1. 缓存当前会话消息列表
|
||||||
queryParams.pageNo = 1
|
kefuStore.saveMessageList(conversation.value.id, messageList.value)
|
||||||
messageList.value = []
|
// 2.1 会话切换,重置相关参数
|
||||||
total.value = 0
|
messageList.value = kefuStore.getConversationMessageList(val.id) || []
|
||||||
|
total.value = messageList.value.length || 0
|
||||||
loadHistory.value = false
|
loadHistory.value = false
|
||||||
refreshContent.value = false
|
refreshContent.value = false
|
||||||
// 设置会话相关属性
|
skipGetMessageList.value = false
|
||||||
|
// 2.2 设置会话相关属性
|
||||||
conversation.value = val
|
conversation.value = val
|
||||||
queryParams.conversationId = val.id
|
queryParams.conversationId = val.id
|
||||||
// 获取消息
|
queryParams.createTime = undefined
|
||||||
|
// 3. 获取消息
|
||||||
await refreshMessageList()
|
await refreshMessageList()
|
||||||
}
|
}
|
||||||
defineExpose({ getNewMessageList, refreshMessageList })
|
defineExpose({ getNewMessageList, refreshMessageList })
|
||||||
|
|
||||||
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
|
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
|
||||||
const skipGetMessageList = computed(() => {
|
const skipGetMessageList = ref(false) // 跳过消息获取
|
||||||
// 已加载到最后一页的话则不触发新的消息获取
|
|
||||||
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
|
|
||||||
}) // 跳过消息获取
|
|
||||||
|
|
||||||
/** 处理表情选择 */
|
/** 处理表情选择 */
|
||||||
const handleEmojiSelect = (item: Emoji) => {
|
const handleEmojiSelect = (item: Emoji) => {
|
||||||
|
@ -256,13 +267,17 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送文本消息 */
|
/** 发送文本消息 */
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async (event: any) => {
|
||||||
|
// shift 不发送
|
||||||
|
if (event.shiftKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// 1. 校验消息是否为空
|
// 1. 校验消息是否为空
|
||||||
if (isEmpty(unref(message.value))) {
|
if (isEmpty(unref(message.value))) {
|
||||||
messageTool.notifyWarning('请输入消息后再发送哦!')
|
messageTool.notifyWarning('请输入消息后再发送哦!')
|
||||||
|
@ -272,7 +287,7 @@ const handleSendMessage = async () => {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -284,6 +299,8 @@ const sendMessage = async (msg: any) => {
|
||||||
message.value = ''
|
message.value = ''
|
||||||
// 加载消息列表
|
// 加载消息列表
|
||||||
await refreshMessageList()
|
await refreshMessageList()
|
||||||
|
// 更新会话缓存
|
||||||
|
await kefuStore.updateConversation(conversation.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 滚动到底部 */
|
/** 滚动到底部 */
|
||||||
|
@ -333,8 +350,6 @@ const handleOldMessage = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadHistory.value = true
|
loadHistory.value = true
|
||||||
// 加载消息列表
|
|
||||||
queryParams.pageNo += 1
|
|
||||||
await getMessageList()
|
await getMessageList()
|
||||||
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
|
||||||
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
|
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
|
||||||
|
@ -357,14 +372,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.kefu {
|
.kefu {
|
||||||
&-title {
|
background-color: #f5f5f5;
|
||||||
border-bottom: #e4e0e0 solid 1px;
|
position: relative;
|
||||||
height: 60px;
|
width: calc(100% - 300px - 260px);
|
||||||
line-height: 60px;
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1px; /* 实际宽度 */
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--el-border-color);
|
||||||
|
transform: scaleX(0.3); /* 缩小宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.kefu-header {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px; /* 初始宽度 */
|
||||||
|
background-color: var(--el-border-color);
|
||||||
|
transform: scaleY(0.3); /* 缩小视觉高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
.newMessageTip {
|
.newMessageTip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -381,21 +433,12 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
.kefu-message {
|
.kefu-message {
|
||||||
margin-left: 20px;
|
background-color: #fff;
|
||||||
position: relative;
|
margin-left: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
&::before {
|
border-top-right-radius: 10px;
|
||||||
content: '';
|
border-bottom-right-radius: 10px;
|
||||||
width: 10px;
|
border-bottom-left-radius: 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,37 +446,25 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
.kefu-message {
|
.kefu-message {
|
||||||
margin-right: 20px;
|
background-color: rgb(206, 223, 255);
|
||||||
position: relative;
|
margin-right: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
&::after {
|
border-top-left-radius: 10px;
|
||||||
content: '';
|
border-bottom-right-radius: 10px;
|
||||||
width: 10px;
|
border-bottom-left-radius: 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息气泡
|
// 消息气泡
|
||||||
.kefu-message {
|
.kefu-message {
|
||||||
color: #a9a9a9;
|
color: #414141;
|
||||||
border-radius: 5px;
|
font-weight: 500;
|
||||||
box-shadow: 3px 3px 5px rgba(220, 220, 220, 0.1);
|
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
text-align: left;
|
//text-align: left;
|
||||||
display: inline-block !important;
|
//display: inline-block !important;
|
||||||
position: relative;
|
//word-break: break-all;
|
||||||
word-break: break-all;
|
|
||||||
background-color: var(--app-content-bg-color);
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -444,24 +475,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
|
||||||
.date-message,
|
.date-message,
|
||||||
.system-message {
|
.system-message {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border-radius: 12rpx;
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
padding: 8rpx 16rpx;
|
border-radius: 8px;
|
||||||
margin-bottom: 16rpx;
|
padding: 0 5px;
|
||||||
//background-color: #e8e8e8;
|
color: #fff;
|
||||||
color: #999;
|
font-size: 10px;
|
||||||
font-size: 24rpx;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-tools {
|
.kefu-footer {
|
||||||
width: 100%;
|
position: relative;
|
||||||
border: var(--el-border-color) solid 1px;
|
display: flex;
|
||||||
border-radius: 10px;
|
flex-direction: column;
|
||||||
height: 44px;
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px; /* 初始宽度 */
|
||||||
|
background-color: var(--el-border-color);
|
||||||
|
transform: scaleY(0.3); /* 缩小视觉高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-tools {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(textarea) {
|
::v-deep(textarea) {
|
||||||
resize: none;
|
resize: none;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-textarea__inner) {
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
|
|
@ -1,5 +1,5 @@
|
||||||
import KeFuConversationList from './KeFuConversationList.vue'
|
import KeFuConversationList from './KeFuConversationList.vue'
|
||||||
import KeFuMessageList from './KeFuMessageList.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 }
|
||||||
|
|
|
@ -0,0 +1,252 @@
|
||||||
|
<!-- 右侧信息:会员信息 + 最近浏览 + 交易订单 -->
|
||||||
|
<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 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">
|
||||||
|
<!-- 最近浏览 -->
|
||||||
|
<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'
|
||||||
|
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>>()
|
||||||
|
const handleClick = async (tab: string) => {
|
||||||
|
activeTab.value = tab
|
||||||
|
await nextTick()
|
||||||
|
await getHistoryList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获得历史数据 */
|
||||||
|
const getHistoryList = async () => {
|
||||||
|
switch (activeTab.value) {
|
||||||
|
case '会员信息':
|
||||||
|
await getUserData()
|
||||||
|
await getUserWallet()
|
||||||
|
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)
|
||||||
|
|
||||||
|
/** 查询用户钱包信息 */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
wallet.value =
|
||||||
|
(await WalletApi.getWallet({ userId: conversation.value.userId })) || 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>
|
||||||
|
.kefu {
|
||||||
|
position: relative;
|
||||||
|
width: 300px !important;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1px; /* 实际宽度 */
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--el-border-color);
|
||||||
|
transform: scaleX(0.3); /* 缩小宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px; /* 初始宽度 */
|
||||||
|
background-color: var(--el-border-color);
|
||||||
|
transform: scaleY(0.3); /* 缩小视觉高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
&-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>
|
|
@ -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: #fff; // 透明色,暗黑模式下也能体现
|
||||||
|
|
||||||
.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,29 @@ function formatOrderStatus(order: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-state {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-box {
|
.pay-box {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
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">
|
||||||
|
<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: #fff;
|
||||||
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>
|
||||||
|
|
|
@ -93,7 +93,7 @@ export const useEmoji = () => {
|
||||||
const emojiFile = getEmojiFileByName(item)
|
const emojiFile = getEmojiFileByName(item)
|
||||||
newData = newData.replace(
|
newData = newData.replace(
|
||||||
item,
|
item,
|
||||||
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}" alt=""/>`
|
`<img style="width: 20px;height: 20px;margin:0 1px 3px 1px;vertical-align: middle;" src="${emojiFile}" alt=""/>`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<el-row :gutter="10">
|
<el-container class="kefu-layout">
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
<el-col :span="6">
|
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
|
||||||
<ContentWrap>
|
|
||||||
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
|
|
||||||
</ContentWrap>
|
|
||||||
</el-col>
|
|
||||||
<!-- 会话详情(选中会话的消息列表) -->
|
<!-- 会话详情(选中会话的消息列表) -->
|
||||||
<el-col :span="12">
|
<KeFuMessageList ref="keFuChatBoxRef" />
|
||||||
<ContentWrap>
|
<!-- 会员信息(选中会话的会员信息) -->
|
||||||
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
|
<MemberInfo ref="memberInfoRef" />
|
||||||
</ContentWrap>
|
</el-container>
|
||||||
</el-col>
|
|
||||||
<!-- 会员足迹(选中会话的会员足迹) -->
|
|
||||||
<el-col :span="6">
|
|
||||||
<ContentWrap>
|
|
||||||
<MemberBrowsingHistory ref="memberBrowsingHistoryRef" />
|
|
||||||
</ContentWrap>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
|
import { KeFuConversationList, KeFuMessageList, MemberInfo } from './components'
|
||||||
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
import { WebSocketMessageTypeConstants } from './components/tools/constants'
|
||||||
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
|
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 { jsonParse } from '@/utils'
|
||||||
|
|
||||||
defineOptions({ name: 'KeFu' })
|
defineOptions({ name: 'KeFu' })
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
const kefuStore = useMallKefuStore() // 客服缓存
|
||||||
|
|
||||||
// ======================= WebSocket start =======================
|
// ======================= WebSocket start =======================
|
||||||
const server = ref(
|
const server = ref(
|
||||||
|
@ -55,7 +46,6 @@ 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)
|
||||||
const type = jsonMessage.type
|
const type = jsonMessage.type
|
||||||
|
@ -65,41 +55,39 @@ watchEffect(() => {
|
||||||
}
|
}
|
||||||
// 2.2 消息类型:KEFU_MESSAGE_TYPE
|
// 2.2 消息类型:KEFU_MESSAGE_TYPE
|
||||||
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
|
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
|
||||||
|
const message = JSON.parse(jsonMessage.content)
|
||||||
// 刷新会话列表
|
// 刷新会话列表
|
||||||
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
|
kefuStore.updateConversation(message.conversationId)
|
||||||
getConversationList()
|
|
||||||
// 刷新消息列表
|
// 刷新消息列表
|
||||||
keFuChatBoxRef.value?.refreshMessageList(JSON.parse(jsonMessage.content))
|
keFuChatBoxRef.value?.refreshMessageList(message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 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) {
|
||||||
// 刷新会话列表
|
// 更新会话已读
|
||||||
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
|
kefuStore.updateConversationStatus(jsonParse(jsonMessage.content))
|
||||||
getConversationList()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// ======================= WebSocket end =======================
|
// ======================= WebSocket end =======================
|
||||||
/** 加载会话列表 */
|
|
||||||
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
|
|
||||||
const getConversationList = () => {
|
|
||||||
keFuConversationRef.value?.getConversationList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 加载指定会话的消息列表 */
|
/** 加载指定会话的消息列表 */
|
||||||
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
|
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
|
||||||
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
|
const memberInfoRef = ref<InstanceType<typeof MemberInfo>>()
|
||||||
const handleChange = (conversation: KeFuConversationRespVO) => {
|
const handleChange = (conversation: KeFuConversationRespVO) => {
|
||||||
keFuChatBoxRef.value?.getNewMessageList(conversation)
|
keFuChatBoxRef.value?.getNewMessageList(conversation)
|
||||||
memberBrowsingHistoryRef.value?.initHistory(conversation)
|
memberInfoRef.value?.initHistory(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getConversationList()
|
/** 加载会话列表 */
|
||||||
|
kefuStore.setConversationList().then(() => {
|
||||||
|
keFuConversationRef.value?.calculationLastMessageTime()
|
||||||
|
})
|
||||||
// 打开 websocket 连接
|
// 打开 websocket 连接
|
||||||
open()
|
open()
|
||||||
})
|
})
|
||||||
|
@ -112,9 +100,13 @@ onBeforeUnmount(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.kefu {
|
.kefu-layout {
|
||||||
height: calc(100vh - 165px);
|
position: absolute;
|
||||||
overflow: auto; /* 确保内容可滚动 */
|
flex: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 定义滚动条样式 */
|
/* 定义滚动条样式 */
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<el-descriptions :column="2">
|
<el-descriptions :class="{ 'kefu-descriptions': column === 1 }" :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 {
|
||||||
|
@ -60,4 +62,23 @@ defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO }>() // 用户信
|
||||||
.cell-item::after {
|
.cell-item::after {
|
||||||
content: ':';
|
content: ':';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kefu-descriptions {
|
||||||
|
::v-deep(.el-descriptions__cell) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.el-descriptions__label {
|
||||||
|
width: 120px;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
flex: 1;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,83 +3,163 @@
|
||||||
<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" class="kefu-descriptions">
|
||||||
|
<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>
|
||||||
|
<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;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep(.kefu-descriptions) {
|
||||||
|
.el-descriptions__cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.el-descriptions__label {
|
||||||
|
width: 120px;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-descriptions__content {
|
||||||
|
flex: 1;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue