Commit d3f30446 by 芋道源码 Committed by Gitee

!576 【功能完善】商城客服

Merge pull request !576 from puhui999/dev-crm
parents 1d01955b 70abc5fc
...@@ -29,8 +29,8 @@ export const KeFuMessageApi = { ...@@ -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 })
} }
} }
import { store } from '@/store'
import { defineStore } from 'pinia'
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
// TODO puhui999: 待优化完善
interface MallKefuInfoVO {
conversationList: KeFuConversationRespVO[] // 会话列表
conversationMessageList: Map<number, KeFuMessageRespVO[]> // 会话消息
}
export const useMallKefuStore = defineStore('mall-kefu', {
state: (): MallKefuInfoVO => ({
conversationList: [],
conversationMessageList: new Map<number, KeFuMessageRespVO[]>() // key 会话,value 会话消息列表
}),
getters: {
getConversationList(): KeFuConversationRespVO[] {
return this.conversationList
},
getConversationMessageList(): Map<number, KeFuMessageRespVO[]> {
return this.conversationMessageList
}
},
actions: {
async setConversationList() {
const list = await KeFuConversationApi.getConversationList()
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
this.conversationList = list
}
// async setConversationMessageList(conversationId: number) {}
}
})
export const useUserStoreWithOut = () => {
return useMallKefuStore(store)
}
<template> <template>
<div class="kefu"> <el-aside class="kefu p-5px h-100%" width="260px">
<div class="color-[#999] font-bold my-10px">会话记录({{ conversationList.length }})</div>
<div <div
v-for="item in conversationList" v-for="item in conversationList"
:key="item.id" :key="item.id"
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
<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') }} {{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
</span> </span>
</div> </div>
...@@ -31,7 +32,7 @@ ...@@ -31,7 +32,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 +66,7 @@ ...@@ -65,7 +66,7 @@
取消 取消
</li> </li>
</ul> </ul>
</div> </el-aside>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
...@@ -180,11 +181,12 @@ watch(showRightMenu, (val) => { ...@@ -180,11 +181,12 @@ watch(showRightMenu, (val) => {
<style lang="scss" scoped> <style lang="scss" scoped>
.kefu { .kefu {
background-color: #fff;
&-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;
...@@ -205,13 +207,10 @@ watch(showRightMenu, (val) => { ...@@ -205,13 +207,10 @@ watch(showRightMenu, (val) => {
} }
} }
.active { .active,
border-left: 5px #3271ff solid;
background-color: var(--login-bg-color);
}
.pinned { .pinned {
background-color: var(--left-menu-bg-active-color); background-color: rgba(128, 128, 128, 0.5); // 透明色,暗黑模式下也能体现
border-radius: 8px;
} }
.right-menu-ul { .right-menu-ul {
......
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 }
<!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 --> <!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 -->
<template> <template>
<div v-show="!isEmpty(conversation)" class="kefu"> <el-container class="kefu">
<div class="header-title h-60px flex justify-center items-center">他的足迹</div> <el-header class="kefu-header">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> <div
<el-tab-pane label="最近浏览" name="a" /> :class="{ 'kefu-header-item-activation': tabActivation('会员信息') }"
<el-tab-pane label="订单列表" name="b" /> class="kefu-header-item cursor-pointer flex items-center justify-center"
</el-tabs> @click="handleClick('会员信息')"
<div> >
<el-scrollbar ref="scrollbarRef" always height="calc(115vh - 400px)" @scroll="handleScroll"> 会员信息
</div>
<div
:class="{ 'kefu-header-item-activation': tabActivation('最近浏览') }"
class="kefu-header-item cursor-pointer flex items-center justify-center"
@click="handleClick('最近浏览')"
>
最近浏览
</div>
<div
:class="{ 'kefu-header-item-activation': tabActivation('交易订单') }"
class="kefu-header-item cursor-pointer flex items-center justify-center"
@click="handleClick('交易订单')"
>
交易订单
</div>
</el-header>
<el-main class="kefu-content">
<div v-show="!isEmpty(conversation)">
<el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
<!-- 最近浏览 --> <!-- 最近浏览 -->
<ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" /> <ProductBrowsingHistory v-if="activeTab === '最近浏览'" ref="productBrowsingHistoryRef" />
<!-- 订单列表 --> <!-- 交易订单 -->
<OrderBrowsingHistory v-if="activeName === 'b'" ref="orderBrowsingHistoryRef" /> <OrderBrowsingHistory v-if="activeTab === '交易订单'" ref="orderBrowsingHistoryRef" />
</el-scrollbar> </el-scrollbar>
</div> </div>
</div>
<el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" /> <el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
</el-main>
</el-container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { TabsPaneContext } from 'element-plus'
import ProductBrowsingHistory from './ProductBrowsingHistory.vue' import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
import OrderBrowsingHistory from './OrderBrowsingHistory.vue' import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation' import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
...@@ -29,25 +48,26 @@ import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrol ...@@ -29,25 +48,26 @@ import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrol
defineOptions({ name: 'MemberBrowsingHistory' }) defineOptions({ name: 'MemberBrowsingHistory' })
const activeName = ref('a') const activeTab = ref('会员信息')
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>>()
const handleClick = async (tab: TabsPaneContext) => { const handleClick = async (tab: string) => {
activeName.value = tab.paneName as string activeTab.value = tab
await nextTick() await nextTick()
await getHistoryList() await getHistoryList()
} }
/** 获得历史数据 */ /** 获得历史数据 */
// TODO @puhui:不要用 a、b 哈。就订单列表、浏览列表这种噶
const getHistoryList = async () => { const getHistoryList = async () => {
switch (activeName.value) { switch (activeTab.value) {
case 'a': case '会员信息':
break
case '最近浏览':
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value) await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
break break
case 'b': case '交易订单':
await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value) await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
break break
default: default:
...@@ -57,11 +77,13 @@ const getHistoryList = async () => { ...@@ -57,11 +77,13 @@ const getHistoryList = async () => {
/** 加载下一页数据 */ /** 加载下一页数据 */
const loadMore = async () => { const loadMore = async () => {
switch (activeName.value) { switch (activeTab.value) {
case 'a': case '会员信息':
break
case '最近浏览':
await productBrowsingHistoryRef.value?.loadMore() await productBrowsingHistoryRef.value?.loadMore()
break break
case 'b': case '交易订单':
await orderBrowsingHistoryRef.value?.loadMore() await orderBrowsingHistoryRef.value?.loadMore()
break break
default: default:
...@@ -72,7 +94,7 @@ const loadMore = async () => { ...@@ -72,7 +94,7 @@ const loadMore = async () => {
/** 浏览历史初始化 */ /** 浏览历史初始化 */
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话 const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
const initHistory = async (val: KeFuConversationRespVO) => { const initHistory = async (val: KeFuConversationRespVO) => {
activeName.value = 'a' activeTab.value = '会员信息'
conversation.value = val conversation.value = val
await nextTick() await nextTick()
await getHistoryList() await getHistoryList()
...@@ -91,6 +113,66 @@ const handleScroll = debounce(() => { ...@@ -91,6 +113,66 @@ const handleScroll = debounce(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.kefu {
width: 300px !important;
background-color: #fff;
border-left: var(--el-border-color) solid 1px;
&-header {
background: #fbfbfb;
box-shadow: 0 0 0 0 #dcdfe6;
display: flex;
align-items: center;
justify-content: space-around;
&-title {
font-size: 18px;
font-weight: bold;
}
&-item {
height: 100%;
width: 100%;
position: relative;
&-activation::before {
content: '';
position: absolute; /* 绝对定位 */
top: 0;
left: 0;
right: 0;
bottom: 0; /* 覆盖整个元素 */
border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
}
&:hover::before {
content: '';
position: absolute; /* 绝对定位 */
top: 0;
left: 0;
right: 0;
bottom: 0; /* 覆盖整个元素 */
border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 边框样式 */
pointer-events: none; /* 确保点击事件不会被伪元素拦截 */
}
}
}
&-content {
margin: 0;
padding: 0;
position: relative;
height: 100%;
width: 100%;
}
&-tabs {
height: 100%;
width: 100%;
}
}
.header-title { .header-title {
border-bottom: #e4e0e0 solid 1px; border-bottom: #e4e0e0 solid 1px;
} }
......
<template> <template>
<el-row :gutter="10"> <el-container class="kefu-layout">
<!-- 会话列表 --> <!-- 会话列表 -->
<el-col :span="6">
<ContentWrap>
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" /> <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
</ContentWrap>
</el-col>
<!-- 会话详情(选中会话的消息列表) --> <!-- 会话详情(选中会话的消息列表) -->
<el-col :span="12">
<ContentWrap>
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" /> <KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
</ContentWrap>
</el-col>
<!-- 会员足迹(选中会话的会员足迹) --> <!-- 会员足迹(选中会话的会员足迹) -->
<el-col :span="6"> <MemberInfo ref="memberInfoRef" />
<ContentWrap> </el-container>
<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'
...@@ -91,10 +79,10 @@ const getConversationList = () => { ...@@ -91,10 +79,10 @@ const 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)
} }
/** 初始化 */ /** 初始化 */
...@@ -112,9 +100,13 @@ onBeforeUnmount(() => { ...@@ -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%;
} }
/* 定义滚动条样式 */ /* 定义滚动条样式 */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment