后台-客服页面调整和客服转接功能

This commit is contained in:
XinWei 2024-10-25 11:23:22 +08:00
parent 8ab64a909a
commit 0e37da8584
15 changed files with 187 additions and 29 deletions

View File

@ -1,4 +1,5 @@
import request from '@/config/axios'
import { number } from 'vue-types'
export interface KeFuConversationRespVO {
id: number // 编号
@ -21,6 +22,10 @@ export const KeFuConversationApi = {
getConversationList: async () => {
return await request.get({ url: '/promotion/kefu-conversation/list' })
},
// 获得客服会话列表
getConversationListByKefuId: async (kefuId: number) => {
return await request.get({ url: '/promotion/kefu-conversation/list?kefuId=' + kefuId })
},
// 客服会话置顶
updateConversationPinned: async (data: any) => {
return await request.put({
@ -30,6 +35,10 @@ export const KeFuConversationApi = {
},
// 删除客服会话
deleteConversation: async (id: number) => {
return await request.get({ url: '/promotion/kefu-conversation/delete?id' + id })
return await request.delete({ url: '/promotion/kefu-conversation/delete?id=' + id })
},
// 转接会话给其它客服
transferConversion: async (id: number, kefuId: number) => {
return await request.get({ url: `/promotion/kefu-conversation/transfer/${id}/${kefuId}`})
}
}

View File

@ -25,6 +25,7 @@ export const SupportStaffApi = {
return await request.get({ url: `/promotion/support-staff/get?id=` + id })
},
// 新增客服人员
createSupportStaff: async (data: SupportStaffVO) => {
return await request.post({ url: `/promotion/support-staff/create`, data })

View File

@ -6,6 +6,7 @@ const { wsCache } = useCache()
const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'
const StaffTokenKey = 'STAFF_TOKEN'
// 获取token
export const getAccessToken = () => {
@ -30,6 +31,16 @@ export const removeToken = () => {
wsCache.delete(RefreshTokenKey)
}
// 获取客服id TODO后面可以完善为token
export const getStaffToken = () => {
// 此处与TokenKey相同此写法解决初始化时Cookies中不存在TokenKey报错
return wsCache.get(StaffTokenKey) ? wsCache.get(StaffTokenKey) : wsCache.get('STAFF_TOKEN')
}
// 设置客服id TODO后面可以完善为token
export const setStaffToken = (id: number) => {
wsCache.set(StaffTokenKey, id)
}
/** 格式化tokenjwt格式 */
export const formatToken = (token: string): string => {
return 'Bearer ' + token

View File

@ -74,6 +74,7 @@ import { useEmoji } from './tools/emoji'
import { formatPast } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from './tools/constants'
import { useAppStore } from '@/store/modules/app'
import { getStaffToken} from '@/utils/auth'
defineOptions({ name: 'KeFuConversationList' })
@ -86,7 +87,7 @@ const collapse = computed(() => appStore.getCollapse) // 折叠菜单
/** 加载会话列表 */
const getConversationList = async () => {
const list = await KeFuConversationApi.getConversationList()
const list = await KeFuConversationApi.getConversationListByKefuId(getStaffToken())
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
conversationList.value = list
}
@ -206,8 +207,9 @@ watch(showRightMenu, (val) => {
}
.active {
border-left: 5px #3271ff solid;
background-color: var(--login-bg-color);
border-left: 5px #0256ff solid;
// background-color: var(--login-bg-color);
background-color: #409eff;
}
.pinned {

View File

@ -10,7 +10,8 @@
<div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
<div class="flex justify-center items-center mb-20px">
<!-- 日期 -->
<div v-if="
<div
v-if="
item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)
" class="date-message">
{{ formatDate(item.createTime) }}
@ -20,16 +21,19 @@
{{ item.content }}
</div>
</div>
<div :class="[
<div
:class="[
item.senderType === UserTypeEnum.MEMBER
? `ss-row-left`
: item.senderType === UserTypeEnum.ADMIN
? `ss-row-right`
: ''
]" class="flex mb-20px w-[100%]">
<el-avatar v-if="item.senderType === UserTypeEnum.MEMBER" :src="conversation.userAvatar"
<el-avatar
v-if="item.senderType === UserTypeEnum.MEMBER" :src="conversation.userAvatar"
alt="avatar" class="w-60px h-60px" />
<div :class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
<div
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
class="p-10px">
<!-- 文本消息 -->
<MessageItem :message="item">
@ -40,13 +44,15 @@
</MessageItem>
<!-- 图片消息 -->
<MessageItem :message="item">
<el-image v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
<el-image
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
:initial-index="0" :preview-src-list="[item.content]" :src="item.content"
class="w-200px" fit="contain" preview-teleported />
</MessageItem>
<!-- 商品消息 -->
<MessageItem :message="item">
<ProductItem v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
<ProductItem
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
:spuId="getMessageContent(item).spuId" :picUrl="getMessageContent(item).picUrl"
:price="getMessageContent(item).price"
:skuText="getMessageContent(item).introduction"
@ -55,17 +61,20 @@
</MessageItem>
<!-- 订单消息 -->
<MessageItem :message="item">
<OrderItem v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
<OrderItem
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
:message="item" class="max-w-100%" />
</MessageItem>
</div>
<el-avatar v-if="item.senderType === UserTypeEnum.ADMIN" :src="item.senderAvatar"
<el-avatar
v-if="item.senderType === UserTypeEnum.ADMIN" :src="item.senderAvatar"
alt="avatar" />
</div>
</div>
</div>
</el-scrollbar>
<div v-show="showNewMessageTip" class="newMessageTip flex items-center cursor-pointer"
<div
v-show="showNewMessageTip" class="newMessageTip flex items-center cursor-pointer"
@click="handleToNewMessage">
<span>有新消息</span>
<Icon class="ml-5px" icon="ep:bottom" />
@ -81,6 +90,19 @@
<div style="margin-left: 9px; margin-top:5px;cursor: pointer;">
<img :src="Picture" class="w-32px h-32px" @click="huashu" />
</div>
<!-- 转接按钮 -->
<el-dropdown placement="top" style="margin-left: auto;margin-right: 15px; margin-top:5px;margin-top:5px;cursor: pointer;" ref="dropdown1" trigger="contextmenu">
<div>
<img :src="Picture2" class="w-27px h-27px" @click="getOnlineStaffList" title="转接"/>
</div>
<template #dropdown>
<el-dropdown-item v-for="staff in onlineStaffList" :key="staff.id" :disabled="staff.id===getStaffToken()" @click="transferConversion(staff.id)">
{{ staff.name }}
</el-dropdown-item>
</template>
</el-dropdown>
</div>
<el-input v-model="message" :rows="6" style="border-style: none" type="textarea" />
<div class="h-45px flex justify-end">
@ -119,6 +141,7 @@
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
// import VerbalTrick from './tools/VerbalTrick.vue'
import Picture from '@/views/mall/promotion/kefu/components/asserts/huashu.png'
import Picture2 from '@/views/mall/promotion/kefu/components/asserts/zhuanjie.png'
import ProductItem from './message/ProductItem.vue'
import OrderItem from './message/OrderItem.vue'
import { Emoji, useEmoji } from './tools/emoji'
@ -130,6 +153,9 @@
import relativeTime from 'dayjs/plugin/relativeTime'
import { debounce } from 'lodash-es'
import { jsonParse } from '@/utils'
import { getStaffToken, setStaffToken} from '@/utils/auth'
import type { DropdownInstance } from 'element-plus'
import { ref } from 'vue'
dayjs.extend(relativeTime)
@ -145,6 +171,10 @@
const messageList = ref<KeFuMessageRespVO[]>([]) //
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const showNewMessageTip = ref(false) //
import { SupportStaffApi, SupportStaffVO } from '@/api/mall/promotion/supportstaff' //
import { KeFuConversationApi} from '@/api/mall/promotion/kefu/conversation'
import { number } from 'vue-types'
const onlineStaffList = ref<SupportStaffVO[]>([]) // 线
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -152,6 +182,7 @@
})
const total = ref(0) //
const refreshContent = ref(false) // ,
const dropdown1 = ref<DropdownInstance>()
/** 获悉消息内容 */
const getMessageContent = computed(() => (item : any) => jsonParse(item.content))
@ -360,8 +391,29 @@
}
return false
})
/** 查询在线客服人员列表 */
const getOnlineStaffList = async () => {
if (!dropdown1.value) return
dropdown1.value.handleOpen()
try {
const data = await SupportStaffApi.getSupportStaffPage({
pageNo: 1,
pageSize: 10,
status: 1,
})
onlineStaffList.value = data.list
} finally {
}
}
/** 转接客服人员列表 id会话id kefuId客服人员id */
const transferConversion = async (kefuId: number) => {
try {
await KeFuConversationApi.transferConversion(queryParams.conversationId, kefuId)
} finally {
// todo
}
}
/** 初始化 **/
onMounted(() => {
getVerbalTrickList()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -153,9 +153,16 @@ const openDetail = (spuId: number) => {
}
.title-text {
font-size: 13px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
display: -webkit-box; /* 对于支持的浏览器 */
-webkit-box-orient: vertical; /* 垂直排列 */
overflow: hidden; /* 隐藏超出部分 */
-webkit-line-clamp: 2; /* 限制为2行 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
line-height: 1.5; /* 行高,可以根据需要调整 */
max-height: 3rem; /* 根据行高和行数设置最大高度2行 * 行高 */
}
.spec-text {

View File

@ -1,19 +1,19 @@
<template>
<el-row :gutter="10">
<el-row :gutter="10" style="display: flex; justify-content: center;">
<!-- 会话列表 -->
<el-col :span="6">
<el-col :span="5" >
<ContentWrap>
<KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
</ContentWrap>
</el-col>
<!-- 会话详情选中会话的消息列表 -->
<el-col :span="12">
<el-col :span="10">
<ContentWrap>
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
</ContentWrap>
</el-col>
<!-- 会员足迹选中会话的会员足迹 -->
<el-col :span="6">
<el-col :span="5">
<ContentWrap>
<MemberBrowsingHistory ref="memberBrowsingHistoryRef" />
</ContentWrap>
@ -42,7 +42,7 @@
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
autoReconnect: true,
autoReconnect: false,
heartbeat: true
})

View File

@ -214,7 +214,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { SupportStaffApi, SupportStaffVO } from '@/api/mall/promotion/supportstaff'
import SupportStaffForm from './SupportStaffForm.vue'
import { setStaffToken} from '@/utils/auth'
/** 客服人员 列表 */
defineOptions({ name: 'SupportStaff' })
@ -285,8 +285,8 @@ const handleDelete = async (id: number) => {
}
/** 客服进入工作台 */
const handleEnterConsole = async (id: number) => {
setStaffToken(id);
window.open(`${window.location.origin}/kefu/kefu`, '_blank');
}
/** 导出按钮操作 */
const handleExport = async () => {

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.conversation.KeFuConversationRespVO;
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.service.kefu.KeFuConversationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -54,16 +56,37 @@ public class KeFuConversationController {
@GetMapping("/list")
@Operation(summary = "获得客服会话列表")
@PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:query')")
public CommonResult<List<KeFuConversationRespVO>> getConversationList() {
// 查询会话列表
List<KeFuConversationRespVO> respList = BeanUtils.toBean(conversationService.getKefuConversationList(),
KeFuConversationRespVO.class);
public CommonResult<List<KeFuConversationRespVO>> getConversationList(@RequestParam(required = false, value = "kefuId") Long kefuId) {
// 如果会话列表中的kefuId为null则转接给当前kefuId处理小程序新会话
if (kefuId != null) {
List<KeFuConversationDO> updateList = new ArrayList<>();
List<KeFuConversationRespVO> allConversationList = BeanUtils.toBean(conversationService.getKefuConversationList(),
KeFuConversationRespVO.class);
for (KeFuConversationRespVO keFuConversationRespVO : allConversationList) {
if (keFuConversationRespVO.getKefuId() == null) {
keFuConversationRespVO.setKefuId(kefuId);
updateList.add(BeanUtils.toBean(keFuConversationRespVO, KeFuConversationDO.class));
}
}
if (!updateList.isEmpty()) {
conversationService.updateConversation(updateList);
}
}
// 查询会话列表
List<KeFuConversationRespVO> respList = BeanUtils.toBean(conversationService.getKefuConversationList(kefuId),
KeFuConversationRespVO.class);
// 拼接数据
Map<Long, MemberUserRespDTO> userMap = memberUserApi.getUserMap(convertSet(respList, KeFuConversationRespVO::getUserId));
respList.forEach(item-> findAndThen(userMap, item.getUserId(),
memberUser-> item.setUserAvatar(memberUser.getAvatar()).setUserNickname(memberUser.getNickname())));
respList.forEach(item -> findAndThen(userMap, item.getUserId(),
memberUser -> item.setUserAvatar(memberUser.getAvatar()).setUserNickname(memberUser.getNickname())));
return success(respList);
}
@GetMapping("/transfer/{id}/{kefuId}")
public CommonResult<Boolean> getSupportStaff(@PathVariable("id") Long id, @PathVariable("kefuId") Long kefuId) {
conversationService.transferConversation(id, kefuId);
// 处理逻辑
return success(true);
}
}

View File

@ -43,4 +43,7 @@ public class KeFuConversationRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "所属客服编号")
private Long kefuId;
}

View File

@ -80,4 +80,9 @@ public class KeFuConversationDO extends BaseDO {
*/
private Integer adminUnreadMessageCount;
/**
* 所属客服编号
*/
private Long kefuId;
}

View File

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

View File

@ -50,11 +50,18 @@ public interface KeFuConversationService {
void updateConversationAdminDeleted(Long id, Boolean adminDeleted);
/**
* 管理员获得客服会话列表
* 管理员获得全部客服会话列表
*
* @return 会话列表
*/
List<KeFuConversationDO> getKefuConversationList();
/**
* 管理员获得客服会话列表
*
* @param kefuId 客服编号
* @return 会话列表
*/
List<KeFuConversationDO> getKefuConversationList(Long kefuId);
/**
* 会员获得或创建会话
@ -82,4 +89,18 @@ public interface KeFuConversationService {
*/
KeFuConversationDO getConversationByUserId(Long userId);
/**
* 更新客服列表
* @param updateList
* @return void
*/
void updateConversation(List<KeFuConversationDO> updateList);
/**
* 转接会话
* @param id 会话id
* @param kefuId 客服id
* @return void
*/
void transferConversation(Long id, Long kefuId);
}

View File

@ -87,6 +87,11 @@ public class KeFuConversationServiceImpl implements KeFuConversationService {
return conversationMapper.selectConversationList();
}
@Override
public List<KeFuConversationDO> getKefuConversationList(Long kefuId) {
return conversationMapper.selectConversationList(kefuId);
}
@Override
public KeFuConversationDO getOrCreateConversation(Long userId) {
KeFuConversationDO conversation = conversationMapper.selectOne(KeFuConversationDO::getUserId, userId);
@ -115,4 +120,17 @@ public class KeFuConversationServiceImpl implements KeFuConversationService {
return conversationMapper.selectByUserId(userId);
}
@Override
public void updateConversation(List<KeFuConversationDO> updateList) {
conversationMapper.updateBatch(updateList);
}
@Override
public void transferConversation(Long id, Long kefuId) {
KeFuConversationDO keFuConversationDO = new KeFuConversationDO();
keFuConversationDO.setId(id);
keFuConversationDO.setKefuId(kefuId);
conversationMapper.updateById(keFuConversationDO);
}
}