优化客服功能 #106

Merged
root merged 1 commits from sjy-two into master 2024-11-06 18:17:12 +08:00
16 changed files with 377 additions and 169 deletions
Showing only changes of commit 6fecdf6878 - Show all commits

View File

@ -20,6 +20,7 @@ export interface UserVO {
point: number | undefined | null point: number | undefined | null
totalPoint: number | undefined | null totalPoint: number | undefined | null
experience: number | null | undefined experience: number | null | undefined
groupName: string
} }
// 查询会员用户列表 // 查询会员用户列表
@ -51,3 +52,8 @@ export const updateUserPoint = async (data: any) => {
export const updateUserBalance = async (data: any) => { export const updateUserBalance = async (data: any) => {
return await request.put({ url: `/member/user/update-balance`, data }) return await request.put({ url: `/member/user/update-balance`, data })
} }
// 客服查询用户详情
export const getUserInfo = async (id: number) => {
return await request.get({ url: `/member/user/getUserInfo?id=` + id })
}

View File

@ -35,88 +35,120 @@ const mobile = computed(() => appStore.getMobile)
// 固定菜单 // 固定菜单
const fixedMenu = computed(() => appStore.getFixedMenu) const fixedMenu = computed(() => appStore.getFixedMenu)
import { useRoute } from 'vue-router'
export const useRenderLayout = () => { export const useRenderLayout = () => {
const renderClassic = () => { const route = useRoute()
return ( let renderClassic = null;
<> if(route.path == "/kefu/kefu"){
<div renderClassic = () => {
class={[ return (
'absolute top-0 left-0 h-full layout-border__right', <>
{ '!fixed z-3000': mobile.value }
]}
>
{logo.value ? (
<Logo
class={[
'bg-[var(--left-menu-bg-color)] relative',
{
'!pl-0': mobile.value && collapse.value,
'w-[var(--left-menu-min-width)]': appStore.getCollapse,
'w-[var(--left-menu-max-width)]': !appStore.getCollapse
}
]}
style="transition: all var(--transition-time-02);"
></Logo>
) : undefined}
<Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
</div>
<div
class={[
`${prefixCls}-content`,
'absolute top-0 h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value && !mobile.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value && !mobile.value && !mobile.value,
'fixed !w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
fixedHeader.value
}
]}
>
<div
class={[
{
'fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
collapse.value && fixedHeader.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
!collapse.value && fixedHeader.value && !mobile.value,
'!w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ToolHeader
class={[
'bg-[var(--top-header-bg-color)]',
{
'layout-border__bottom': !tagsView.value
}
]}
></ToolHeader>
{tagsView.value ? ( <div
<TagsView class="layout-border__top layout-border__bottom"></TagsView>
) : undefined}
</div>
<AppView></AppView> style="transition: all var(--transition-time-02);width:85%;margin:0 auto;"
</ElScrollbar> >
</div> <ElScrollbar
</> v-loading={pageLoading.value}
) class={[
} `${prefixCls}-content-scrollbar`,
{
}
]}
>
<AppView></AppView>
</ElScrollbar>
</div>
</>
)
}
}else {
renderClassic = () => {
return (
<>
<div
class={[
'absolute top-0 left-0 h-full layout-border__right',
{ '!fixed z-3000': mobile.value }
]}
>
{logo.value ? (
<Logo
class={[
'bg-[var(--left-menu-bg-color)] relative',
{
'!pl-0': mobile.value && collapse.value,
'w-[var(--left-menu-min-width)]': appStore.getCollapse,
'w-[var(--left-menu-max-width)]': !appStore.getCollapse
}
]}
style="transition: all var(--transition-time-02);"
></Logo>
) : undefined}
<Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
</div>
<div
class={[
`${prefixCls}-content`,
'absolute top-0 h-[100%]',
{
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
collapse.value && !mobile.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
!collapse.value && !mobile.value && !mobile.value,
'fixed !w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ElScrollbar
v-loading={pageLoading.value}
class={[
`${prefixCls}-content-scrollbar`,
{
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
fixedHeader.value
}
]}
>
<div
class={[
{
'fixed top-0 left-0 z-10': fixedHeader.value,
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
collapse.value && fixedHeader.value && !mobile.value,
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
!collapse.value && fixedHeader.value && !mobile.value,
'!w-full !left-0': mobile.value
}
]}
style="transition: all var(--transition-time-02);"
>
<ToolHeader
class={[
'bg-[var(--top-header-bg-color)]',
{
'layout-border__bottom': !tagsView.value
}
]}
></ToolHeader>
{tagsView.value ? (
<TagsView class="layout-border__top layout-border__bottom"></TagsView>
) : undefined}
</div>
<AppView></AppView>
</ElScrollbar>
</div>
</>
)
}
}
const renderTopLeft = () => { const renderTopLeft = () => {
return ( return (

View File

@ -1,11 +1,14 @@
<template> <template>
<el-container v-if="showKeFuMessageList" class="kefu"> <el-container v-if="showKeFuMessageList" class="kefu">
<el-header> <!-- <el-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 height="calc(100vh - 420px)" @scroll="handleScroll">
<div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px"> <div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
<!-- 消息列表 --> <!-- 消息列表 -->
<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">
@ -20,6 +23,8 @@
{{ item.content }} {{ item.content }}
</div> </div>
</div> </div>
<div :class="[ <div :class="[
item.senderType === UserTypeEnum.MEMBER item.senderType === UserTypeEnum.MEMBER
? `ss-row-left` ? `ss-row-left`
@ -63,6 +68,10 @@
alt="avatar" /> alt="avatar" />
</div> </div>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div v-show="showNewMessageTip" class="newMessageTip flex items-center cursor-pointer" <div v-show="showNewMessageTip" class="newMessageTip flex items-center cursor-pointer"
@ -78,15 +87,15 @@
<PictureSelectUpload class="ml-15px mt-3px cursor-pointer" @send-picture="handleSendPicture" /> <PictureSelectUpload class="ml-15px mt-3px cursor-pointer" @send-picture="handleSendPicture" />
<!-- <VerbalTrick class="ml-11px mt-5px cursor-pointer" /> --> <!-- <VerbalTrick class="ml-11px mt-5px cursor-pointer" /> -->
<!-- 话术库 --> <!-- 话术库 -->
<div style="margin-left: 9px; margin-top:5px;cursor: pointer;"> <div style="margin-left: 15px; margin-top:4px;cursor: pointer;">
<img :src="Picture" class="w-32px h-32px" @click="huashu" /> <img :src="xiaoxi" class="w-22px h-22px" @click="huashu" />
</div> </div>
<!-- 转接按钮 --> <!-- 转接按钮 -->
<el-dropdown placement="top" <el-dropdown placement="top"
style="margin-left: auto;margin-right: 15px; margin-top:5px;margin-top:5px;cursor: pointer;" style="margin-left: auto;margin-right: 15px;cursor: pointer;"
ref="dropdown1" trigger="contextmenu"> ref="dropdown1" trigger="contextmenu">
<div> <div>
<img :src="Picture2" class="w-27px h-27px" @click="getOnlineStaffList" title="转接" /> <img :src="Picture2" class="w-62px h-20px" @click="getOnlineStaffList" title="转接" />
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-item v-for="staff in onlineStaffList" :key="staff.id" <el-dropdown-item v-for="staff in onlineStaffList" :key="staff.id"
@ -140,7 +149,8 @@
import PictureSelectUpload from './tools/PictureSelectUpload.vue' import PictureSelectUpload from './tools/PictureSelectUpload.vue'
// import VerbalTrick from './tools/VerbalTrick.vue' // import VerbalTrick from './tools/VerbalTrick.vue'
import Picture from '@/views/mall/promotion/kefu/components/asserts/huashu.png' import Picture from '@/views/mall/promotion/kefu/components/asserts/huashu.png'
import Picture2 from '@/views/mall/promotion/kefu/components/asserts/zhuanjie.png' import xiaoxi from '@/views/mall/promotion/kefu/components/asserts/xiaoxi.png'
import Picture2 from '@/views/mall/promotion/kefu/components/asserts/zj.png'
import ProductItem from './message/ProductItem.vue' import ProductItem from './message/ProductItem.vue'
import OrderItem from './message/OrderItem.vue' import OrderItem from './message/OrderItem.vue'
import { Emoji, useEmoji } from './tools/emoji' import { Emoji, useEmoji } from './tools/emoji'
@ -178,6 +188,8 @@
import { KeFuConversationApi } from '@/api/mall/promotion/kefu/conversation' import { KeFuConversationApi } from '@/api/mall/promotion/kefu/conversation'
import { number } from 'vue-types' import { number } from 'vue-types'
const onlineStaffList = ref<SupportStaffVO[]>([]) // 线 const onlineStaffList = ref<SupportStaffVO[]>([]) // 线
const messages = useMessage() //
const kefuName = ref('')
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
@ -250,6 +262,7 @@
/** 获得新会话的消息列表 */ /** 获得新会话的消息列表 */
// TODO @puhui999 list merge // TODO @puhui999 list merge
const getNewMessageList = async (val : KeFuConversationRespVO) => { const getNewMessageList = async (val : KeFuConversationRespVO) => {
// console.log('22222222',val)
// , // ,
queryParams.pageNo = 1 queryParams.pageNo = 1
messageList.value = [] messageList.value = []
@ -309,7 +322,6 @@
dialogVisible.value = false; dialogVisible.value = false;
} }
const clickMenu = (id : string) => { const clickMenu = (id : string) => {
console.log('1111111111', id)
getVerbalTrickList(id) getVerbalTrickList(id)
} }
@ -417,6 +429,7 @@
status: 1, status: 1,
}) })
onlineStaffList.value = data.list onlineStaffList.value = data.list
} finally { } finally {
} }
} }
@ -424,7 +437,16 @@
/** 转接客服人员列表 id会话id kefuId客服人员id */ /** 转接客服人员列表 id会话id kefuId客服人员id */
const transferConversion = async (kefuId : number) => { const transferConversion = async (kefuId : number) => {
try { try {
await KeFuConversationApi.transferConversion(queryParams.conversationId, kefuId) const re = await KeFuConversationApi.transferConversion(queryParams.conversationId, kefuId)
kefuName.value = re
// 2.
const msg = {
conversationId: conversation.value.id,
contentType: KeFuMessageContentTypeEnum.SYSTEM,
content: '已为您转接至' + re,
}
await sendMessage(msg)
messages.success('转接成功')
} finally { } finally {
// todo // todo
} }

View File

@ -0,0 +1,62 @@
<template>
<div style="height: 522px ;" ref="userInfoRef" >
<div>
<span style="display: flex;">
<el-avatar
style="border: 1px solid #f8f9ee;"
src="https://zysc.fjptzykj.com:3000/shangcheng/29e07b7973621a1a9679744e18f2b4efd1fd50d857fe594879e813d67be3e2db.png"
/>
<span style="margin-left:5px;margin-top: 9px;">Marvin</span>
</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
<div>
<span style="color: #5d5d59;font-size: 13px ;">手机号</span><span style="margin-left: 47px;font-size: 14px ;">暂无</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">分组</span><span style="margin-left: 60px;font-size: 14px ;">D类客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">用户标签</span><span style="margin-left: 33px;font-size: 14px ;">小客户</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
<div>
<span style="color: #5d5d59;font-size: 13px ;">用户等级</span><span style="margin-left: 35px;font-size: 14px ;">暂无</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">推荐人</span><span style="margin-left: 47px;font-size: 14px ;">客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">用户类型</span><span style="margin-left: 33px;font-size: 14px ;">小客户</span>
</div>
<div>
<span style="color: #5d5d59;font-size: 13px ;">余额</span><span style="margin-left: 60px;font-size: 14px ;">暂无</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">推广员</span><span style="margin-left: 47px;font-size: 14px ;">客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">生日</span><span style="margin-left: 60px;font-size: 14px ;">小客户</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
</div>
</template>
<script >
import * as UserApi from '@/api/member/user'
// const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
// const getUserId = async (id: string) => {
// user.value = await UserApi.getUserInfo(id)
// }
// defineExpose({ getUserId })
</script>
<style>
</style>

View File

@ -1,5 +1,6 @@
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 MemberBrowsingHistory from './history/MemberBrowsingHistory.vue'
import UserInfo from './UserInfo.vue'
export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory ,UserInfo}

View File

@ -2,7 +2,8 @@
<template> <template>
<el-popover :width="500" placement="top" trigger="click"> <el-popover :width="500" placement="top" trigger="click">
<template #reference> <template #reference>
<Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" /> <!-- <Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" /> -->
<img :src="biaoqing" style="margin-left: 10px;" class="w-23px h-23px" />
</template> </template>
<ElScrollbar height="300px"> <ElScrollbar height="300px">
<ul class="ml-2 flex flex-wrap px-2"> <ul class="ml-2 flex flex-wrap px-2">
@ -26,8 +27,10 @@
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({ name: 'EmojiSelectPopover' }) defineOptions({ name: 'EmojiSelectPopover' })
import biaoqing from '@/views/mall/promotion/kefu/components/asserts/biaoqian.png'
import { Emoji, useEmoji } from './emoji' import { Emoji, useEmoji } from './emoji'
const { getEmojiList } = useEmoji() const { getEmojiList } = useEmoji()
const emojiList = computed(() => getEmojiList()) const emojiList = computed(() => getEmojiList())

View File

@ -1,12 +1,12 @@
<!-- 图片选择 --> <!-- 图片选择 -->
<template> <template>
<div> <div>
<img :src="Picture" class="w-35px h-35px" @click="selectAndUpload" /> <img :src="tupian" class="w-23px h-23px" @click="selectAndUpload" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Picture from '@/views/mall/promotion/kefu/components/asserts/picture.svg' import tupian from '@/views/mall/promotion/kefu/components/asserts/tupian.png'
import * as FileApi from '@/api/infra/file' import * as FileApi from '@/api/infra/file'
defineOptions({ name: 'PictureSelectUpload' }) defineOptions({ name: 'PictureSelectUpload' })

View File

@ -16,10 +16,10 @@
<el-col :span="6"> <el-col :span="6">
<span style="display: flex; margin-top: 15px;"> <span style="display: flex; margin-top: 15px;">
<el-avatar <el-avatar
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" :src="pic"
/> />
<span style="margin-left:5px;margin-top: 9px;">客服桃子</span> <span style="margin-left:5px;margin-top: 9px;">{{name}}</span>
<el-switch <!-- <el-switch
style="margin-top: 4px;--el-switch-on-color: #13ce66; --el-switch-off-color: #b6bac1;" style="margin-top: 4px;--el-switch-on-color: #13ce66; --el-switch-off-color: #b6bac1;"
v-model="value6" v-model="value6"
class="ml-2" class="ml-2"
@ -27,11 +27,11 @@
inline-prompt inline-prompt
active-text="在线" active-text="在线"
inactive-text="下线" inactive-text="下线"
/> /> -->
</span> </span>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-button size="small" round style="margin-top:23px;margin-left:75%">退出登录</el-button> <el-button @click="out" size="small" round style="margin-top:23px;margin-left:75%">退出</el-button>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
@ -41,14 +41,11 @@
active-text-color="white" active-text-color="white"
style="width:100%;display: flex;" style="width:100%;display: flex;"
> >
<el-menu-item style="width:33%;height:70px" index="1">客户信息</el-menu-item> <el-menu-item @click="userInfo" style="width:33%;height:70px" index="1">客户信息</el-menu-item>
<el-menu-item style="width:33%;height:70px" index="2">交易订单</el-menu-item> <el-menu-item @click="zuoji" style="width:33%;height:70px" index="2">他的足迹</el-menu-item>
<el-menu-item style="width:34%;height:70px" index="3">商品信息</el-menu-item> <!-- <el-menu-item style="width:34%;height:70px" index="3">商品信息</el-menu-item> -->
</el-menu> </el-menu>
<!-- <div style="color: #ffffff;" class="header-title h-60px flex justify-center items-center">他的足迹</div> -->
</el-col> </el-col>
</el-row> </el-row>
@ -74,9 +71,55 @@
<!-- 会员足迹选中会话的会员足迹 --> <!-- 会员足迹选中会话的会员足迹 -->
<el-col :span="6"> <el-col :span="6">
<ContentWrap> <ContentWrap v-show="chick == '2'">
<MemberBrowsingHistory ref="memberBrowsingHistoryRef"/> <MemberBrowsingHistory ref="memberBrowsingHistoryRef"/>
</ContentWrap> </ContentWrap>
<ContentWrap v-show = "chick == '1'">
<div style="height: 522px ;" >
<div>
<span style="display: flex;">
<el-avatar
style="border: 1px solid #f8f9ee;"
:src="user.avatar"
/>
<span style="margin-left:5px;margin-top: 9px;">{{user.nickname}}</span>
</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
<div>
<span style="color: #5d5d59;font-size: 13px ;">手机号</span><span style="margin-left: 47px;font-size: 14px ;">{{user.mobile}}</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">分组</span><span style="margin-left: 60px;font-size: 14px ;">{{user.groupName}}</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">用户标签</span><span style="margin-left: 33px;font-size: 14px ;">小客户</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
<div>
<span style="color: #5d5d59;font-size: 13px ;">用户等级</span><span style="margin-left: 35px;font-size: 14px ;">{{user.levelName}}</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">推荐人</span><span style="margin-left: 47px;font-size: 14px ;">客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">用户类型</span><span style="margin-left: 33px;font-size: 14px ;">小客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">积分</span><span style="margin-left: 60px;font-size: 14px ;">{{user.point}}</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">推广员</span><span style="margin-left: 47px;font-size: 14px ;">客户</span>
</div>
<div style="margin-top: 5px;">
<span style="color: #5d5d59;font-size: 13px ;">生日</span><span style="margin-left: 60px;font-size: 14px ;">{{user.birthday}}</span>
</div>
<el-divider style="border-color: #f5f5f5;margin-top: 15px;margin-bottom:15px;" />
</div>
</ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@ -84,12 +127,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import {KeFuConversationList, KeFuMessageList, MemberBrowsingHistory} from './components' import { KeFuConversationList, KeFuMessageList,MemberBrowsingHistory,UserInfo } 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, getAccessToken} from '@/utils/auth' import {getRefreshToken, getAccessToken} from '@/utils/auth'
import {useWebSocket} from '@vueuse/core' import {useWebSocket} from '@vueuse/core'
import {Search} from '@element-plus/icons-vue' import {Search} from '@element-plus/icons-vue'
import * as UserApi from '@/api/member/user'
defineOptions({name: 'KeFu'}) defineOptions({name: 'KeFu'})
@ -97,6 +143,15 @@
const message = useMessage() // const message = useMessage() //
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
const pic = params.get('pic');
const conversations = ref<KeFuConversationRespVO[]>([])
// const userInfoRef = ref<InstanceType<typeof UserInfo>>()
const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
const userId = ref(0)
// ======================= WebSocket start ======================= // ======================= WebSocket start =======================
const server = ref( const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
@ -104,6 +159,19 @@
getRefreshToken() // 使 getRefreshToken() 使 getAccessToken() WebSocket 便访 getRefreshToken() // 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket ) // WebSocket
const chick = ref('2')
const userInfo = async () =>{
chick.value = '1'
user.value = await UserApi.getUserInfo(userId.value)
}
const zuoji = () =>{
chick.value = '2'
// keFuChatBoxRef.value?.getNewMessageList(conversations.value)
memberBrowsingHistoryRef.value?.initHistory(conversations.value)
}
/** 发起 WebSocket 连接 */ /** 发起 WebSocket 连接 */
const {data, close, open} = useWebSocket(server.value, { const {data, close, open} = useWebSocket(server.value, {
autoReconnect: false, autoReconnect: false,
@ -158,10 +226,18 @@
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>() const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>() const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
const handleChange = (conversation: KeFuConversationRespVO) => { const handleChange = (conversation: KeFuConversationRespVO) => {
conversations.value = conversation
chick.value = '2'
userId.value = conversation.userId
keFuChatBoxRef.value?.getNewMessageList(conversation) keFuChatBoxRef.value?.getNewMessageList(conversation)
memberBrowsingHistoryRef.value?.initHistory(conversation) memberBrowsingHistoryRef.value?.initHistory(conversation)
} }
const out = () =>{
window.close();
// window.location.href = '/kefu/support-staff';
}
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
getConversationList() getConversationList()

View File

@ -35,15 +35,6 @@
class="!w-240px" class="!w-240px"
/> />
</el-form-item> </el-form-item>
<!-- <el-form-item label="登录密码" prop="password">
<el-input
v-model="queryParams.password"
placeholder="请输入登录密码"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> -->
<el-form-item label="客服状态" prop="status"> <el-form-item label="客服状态" prop="status">
<el-select <el-select
v-model="queryParams.status" v-model="queryParams.status"
@ -59,36 +50,6 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item label="手机订单管理" prop="orderManage">
<el-select
v-model="queryParams.orderManage"
placeholder="请选择手机订单管理"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.KEFU_SUPPORT_STAFF_ORDER_MANAGE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item> -->
<!-- <el-form-item label="订单通知" prop="orderInform">
<el-select
v-model="queryParams.orderInform"
placeholder="请选择订单通知"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.KEFU_SUPPORT_STAFF_ORDER_INFORM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item> -->
<el-form-item label="创建时间" prop="createTime"> <el-form-item label="创建时间" prop="createTime">
<el-date-picker <el-date-picker
v-model="queryParams.createTime" v-model="queryParams.createTime"
@ -143,22 +104,11 @@
</el-table-column> </el-table-column>
<el-table-column label="手机号码" align="center" prop="phone" /> <el-table-column label="手机号码" align="center" prop="phone" />
<el-table-column label="登录账号" align="center" prop="account" /> <el-table-column label="登录账号" align="center" prop="account" />
<!-- <el-table-column label="登录密码" align="center" prop="password" /> -->
<el-table-column label="客服状态" align="center" prop="status"> <el-table-column label="客服状态" align="center" prop="status">
<template #default="scope"> <template #default="scope">
<dict-tag :type="DICT_TYPE.KEFU_SUPPORT_STAFF_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.KEFU_SUPPORT_STAFF_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<!-- <el-table-column label="手机订单管理" align="center" prop="orderManage">
<template #default="scope">
<dict-tag :type="DICT_TYPE.KEFU_SUPPORT_STAFF_ORDER_MANAGE" :value="scope.row.orderManage" />
</template>
</el-table-column>
<el-table-column label="订单通知" align="center" prop="orderInform">
<template #default="scope">
<dict-tag :type="DICT_TYPE.KEFU_SUPPORT_STAFF_ORDER_INFORM" :value="scope.row.orderInform" />
</template>
</el-table-column> -->
<el-table-column <el-table-column
label="创建时间" label="创建时间"
align="center" align="center"
@ -188,7 +138,7 @@
v-if="scope.row.status == 1" v-if="scope.row.status == 1"
link link
type="success" type="success"
@click="handleEnterConsole(scope.row.id)" @click="handleEnterConsole(scope.row.id,scope.row.name,scope.row.pic)"
> >
进入工作台 进入工作台
</el-button> </el-button>
@ -206,6 +156,9 @@
<!-- 表单弹窗添加/修改 --> <!-- 表单弹窗添加/修改 -->
<SupportStaffForm ref="formRef" @success="getList" /> <SupportStaffForm ref="formRef" @success="getList" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -215,6 +168,7 @@ import download from '@/utils/download'
import { SupportStaffApi, SupportStaffVO } from '@/api/mall/promotion/supportstaff' import { SupportStaffApi, SupportStaffVO } from '@/api/mall/promotion/supportstaff'
import SupportStaffForm from './SupportStaffForm.vue' import SupportStaffForm from './SupportStaffForm.vue'
import { setStaffToken} from '@/utils/auth' import { setStaffToken} from '@/utils/auth'
import { createImageViewer } from '@/components/ImageViewer'
/** 客服人员 列表 */ /** 客服人员 列表 */
defineOptions({ name: 'SupportStaff' }) defineOptions({ name: 'SupportStaff' })
@ -271,6 +225,13 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id) formRef.value.open(type, id)
} }
const imagePreview = (imgUrl: string) => {
createImageViewer({
zIndex: 9999999,
urlList: [imgUrl]
})
}
/** 删除按钮操作 */ /** 删除按钮操作 */
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
try { try {
@ -284,9 +245,10 @@ const handleDelete = async (id: number) => {
} catch {} } catch {}
} }
/** 客服进入工作台 */ /** 客服进入工作台 */
const handleEnterConsole = async (id: number) => { const handleEnterConsole = async (id: number,name: string,pic: string) => {
setStaffToken(id); setStaffToken(id);
window.open(`${window.location.origin}/kefu/kefu`, '_blank'); const url = `${window.location.origin}/kefu/kefu?name=${encodeURIComponent(name)}&pic=${encodeURIComponent(pic)}`;
window.open(url, '_blank');
} }
/** 导出按钮操作 */ /** 导出按钮操作 */
const handleExport = async () => { const handleExport = async () => {

View File

@ -84,9 +84,9 @@ public class KeFuConversationController {
} }
@Operation(summary = "转接会话给指定客服") @Operation(summary = "转接会话给指定客服")
@GetMapping("/transfer/{id}/{kefuId}") @GetMapping("/transfer/{id}/{kefuId}")
public CommonResult<Boolean> transferConversation(@PathVariable("id") Long id, @PathVariable("kefuId") Long kefuId) { public CommonResult<String> transferConversation(@PathVariable("id") Long id, @PathVariable("kefuId") Long kefuId) {
conversationService.transferConversation(id, kefuId); String name = conversationService.transferConversation(id, kefuId);
// 处理逻辑 // 处理逻辑
return success(true); return success(name);
} }
} }

View File

@ -19,13 +19,13 @@ public interface KeFuConversationMapper extends BaseMapperX<KeFuConversationDO>
default List<KeFuConversationDO> selectConversationList() { default List<KeFuConversationDO> selectConversationList() {
return selectList(new LambdaQueryWrapperX<KeFuConversationDO>() return selectList(new LambdaQueryWrapperX<KeFuConversationDO>()
.eq(KeFuConversationDO::getAdminDeleted, Boolean.FALSE) .eq(KeFuConversationDO::getAdminDeleted, Boolean.FALSE)
.orderByDesc(KeFuConversationDO::getCreateTime)); .orderByDesc(KeFuConversationDO::getLastMessageTime));
} }
default List<KeFuConversationDO> selectConversationList(Long kefuId) { default List<KeFuConversationDO> selectConversationList(Long kefuId) {
return selectList(new LambdaQueryWrapperX<KeFuConversationDO>() return selectList(new LambdaQueryWrapperX<KeFuConversationDO>()
.eq(KeFuConversationDO::getAdminDeleted, Boolean.FALSE) .eq(KeFuConversationDO::getAdminDeleted, Boolean.FALSE)
.eqIfPresent(KeFuConversationDO::getKefuId, kefuId) .eqIfPresent(KeFuConversationDO::getKefuId, kefuId)
.orderByDesc(KeFuConversationDO::getCreateTime)); .orderByDesc(KeFuConversationDO::getLastMessageTime));
} }
default void updateAdminUnreadMessageCountIncrement(Long id) { default void updateAdminUnreadMessageCountIncrement(Long id) {

View File

@ -102,5 +102,5 @@ public interface KeFuConversationService {
* @param kefuId 客服id * @param kefuId 客服id
* @return void * @return void
*/ */
void transferConversation(Long id, Long kefuId); String transferConversation(Long id, Long kefuId);
} }

View File

@ -5,7 +5,9 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO; import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationUpdatePinnedReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO; import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.supportstaff.SupportStaffDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuConversationMapper; import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuConversationMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.supportstaff.SupportStaffMapper;
import cn.iocoder.yudao.module.promotion.enums.kefu.KeFuMessageContentTypeEnum; import cn.iocoder.yudao.module.promotion.enums.kefu.KeFuMessageContentTypeEnum;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -30,6 +32,9 @@ public class KeFuConversationServiceImpl implements KeFuConversationService {
@Resource @Resource
private KeFuConversationMapper conversationMapper; private KeFuConversationMapper conversationMapper;
@Resource
private SupportStaffMapper supportStaffMapper;
@Override @Override
public void deleteKefuConversation(Long id) { public void deleteKefuConversation(Long id) {
// 校验存在 // 校验存在
@ -126,11 +131,13 @@ public class KeFuConversationServiceImpl implements KeFuConversationService {
} }
@Override @Override
public void transferConversation(Long id, Long kefuId) { public String transferConversation(Long id, Long kefuId) {
KeFuConversationDO keFuConversationDO = new KeFuConversationDO(); KeFuConversationDO keFuConversationDO = new KeFuConversationDO();
keFuConversationDO.setId(id); keFuConversationDO.setId(id);
keFuConversationDO.setKefuId(kefuId); keFuConversationDO.setKefuId(kefuId);
conversationMapper.updateById(keFuConversationDO); conversationMapper.updateById(keFuConversationDO);
SupportStaffDO supportStaffDO = supportStaffMapper.selectById(kefuId);
return supportStaffDO.getName();
} }
} }

View File

@ -17,7 +17,10 @@ import cn.iocoder.yudao.module.member.dal.dataobject.level.MemberLevelDO;
import cn.iocoder.yudao.module.member.dal.dataobject.memberCode.MemberCodeDo; import cn.iocoder.yudao.module.member.dal.dataobject.memberCode.MemberCodeDo;
import cn.iocoder.yudao.module.member.dal.dataobject.tag.MemberTagDO; import cn.iocoder.yudao.module.member.dal.dataobject.tag.MemberTagDO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import cn.iocoder.yudao.module.member.dal.mysql.group.MemberGroupMapper;
import cn.iocoder.yudao.module.member.dal.mysql.level.MemberLevelMapper;
import cn.iocoder.yudao.module.member.dal.mysql.memberCode.MemberCodeMapper; import cn.iocoder.yudao.module.member.dal.mysql.memberCode.MemberCodeMapper;
import cn.iocoder.yudao.module.member.dal.mysql.tag.MemberTagMapper;
import cn.iocoder.yudao.module.member.enums.point.MemberPointBizTypeEnum; import cn.iocoder.yudao.module.member.enums.point.MemberPointBizTypeEnum;
import cn.iocoder.yudao.module.member.service.clubCard.ClubCardService; import cn.iocoder.yudao.module.member.service.clubCard.ClubCardService;
import cn.iocoder.yudao.module.member.service.group.MemberGroupService; import cn.iocoder.yudao.module.member.service.group.MemberGroupService;
@ -52,6 +55,15 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLogi
@Validated @Validated
public class MemberUserController { public class MemberUserController {
@Resource
private MemberLevelMapper memberLevelMapper;
@Resource
private MemberGroupMapper memberGroupMapper;
@Resource
private MemberTagMapper memberTagMapper;
@Resource @Resource
private MemberUserService memberUserService; private MemberUserService memberUserService;
@Resource @Resource
@ -169,4 +181,23 @@ public class MemberUserController {
memberCodeMapper.insert(memberCodeDo); memberCodeMapper.insert(memberCodeDo);
return success(uuid); return success(uuid);
} }
@GetMapping("/getUserInfo")
public CommonResult<MemberUserDO> getUserInfo(Long id){
MemberUserDO user = memberUserService.getUser(id);
if (user.getGroupId() != null){
MemberGroupDO groupDO = memberGroupMapper.selectOne("id", user.getGroupId());
user.setGroupName(groupDO.getName());
}
if (user.getLevelId() != null && user.getLevelId() != 0){
MemberLevelDO levelDO = memberLevelMapper.selectOne("id", user.getLevelId());
user.setLevelName(levelDO.getName());
}
return success(user);
}
} }

View File

@ -131,6 +131,9 @@ public class MemberUserDO extends TenantBaseDO {
* 关联 {@link MemberLevelDO#getId()} 字段 * 关联 {@link MemberLevelDO#getId()} 字段
*/ */
private Long levelId; private Long levelId;
@TableField(exist = false)
private String levelName;
/** /**
* 会员经验 * 会员经验
*/ */
@ -142,6 +145,9 @@ public class MemberUserDO extends TenantBaseDO {
*/ */
private Long groupId; private Long groupId;
@TableField(exist = false)
private String groupName;
/** /**
* 是否绑过卡,是否开通过会员(0未开通,1试用,2有效期,3永久,4过期) * 是否绑过卡,是否开通过会员(0未开通,1试用,2有效期,3永久,4过期)
*/ */