添加小程序广告开屏数据配置,客服页面优化

This commit is contained in:
sonjinyon 2024-11-18 09:14:25 +08:00
parent 58a27586a3
commit 6f8bd932f1
49 changed files with 929 additions and 565 deletions

View File

@ -136,7 +136,7 @@
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
<!-- <img src="/logo.gif" class="app-loading-logo" alt="Logo" />-->
<div class="app-loading-title">%VITE_APP_TITLE%</div>
</div>
<div class="app-loading-item">

View File

@ -3,7 +3,7 @@ import request from '@/config/axios'
// 客服配置 VO
export interface ConfigurationVO {
id: number // id
type: string // 客服类型
type: number // 客服类型
feedback: string // 客服反馈
phone: string // 客服电话
link: string // 客服链接

View File

@ -23,8 +23,8 @@ export const KeFuConversationApi = {
return await request.get({ url: '/promotion/kefu-conversation/list' })
},
// 获得客服会话列表
getConversationListByKefuId: async (kefuId: number) => {
return await request.get({ url: '/promotion/kefu-conversation/list?kefuId=' + kefuId })
getConversationListByKefuId: async (kefuId: number , name: string) => {
return await request.get({ url: '/promotion/kefu-conversation/list?kefuId=' + kefuId + `&name=` + name })
},
// 客服会话置顶
updateConversationPinned: async (data: any) => {

View File

@ -32,5 +32,10 @@ export const KeFuMessageApi = {
// 获得消息分页数据
getKeFuMessagePage: async (params: any) => {
return await request.get({ url: '/promotion/kefu-message/page', params })
},
// 获得消息分页数据
getBySenderIdStat: async (senderId: number) => {
return await request.get({ url: '/promotion/kefu-message/getBySenderIdStat?senderId=' + senderId })
}
}

View File

@ -120,7 +120,8 @@
<IFrame class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!" :src="previewUrl" />
<div class="flex flex-col">
<el-text>手机扫码预览</el-text>
<Qrcode :text="previewUrl" logo="/logo.gif" />
<Qrcode :text="previewUrl" />
<!-- <Qrcode :text="previewUrl" logo="/logo.gif" />-->
</div>
</div>
</Dialog>

View File

@ -124,6 +124,9 @@ export enum DICT_TYPE {
TECHNICIAN_STATUS = 'technician_status',
SEX = 'sex',
//客服配置类型
KEFU_CONFIGURATION_TYPE = 'kefu_configuration_type',
INFRA_FILE_TYPE = 'infra_file_type',
PAY_WALLET_RECHARGE_PAY_STATUS = 'pay_wallet_recharge_pay_status',

View File

@ -60,11 +60,11 @@
<Icon icon="ep:upload" class="mr-5px"/>
上传文件
</el-button>
<img @click="liebiao" style="cursor: pointer;margin-left: 240px;" class="mr-10px h-27px w-27px"
src="@/assets/imgs/liebiao.png"/>
<img @click="tubiao" style="cursor: pointer;" class="mr-10px h-27px w-27px"
src="@/assets/imgs/tubiao.png"/>
</el-form-item>
<span><img @click="liebiao" style="cursor: pointer;" class="mr-10px h-30px w-30px"
src="@/assets/imgs/liebiao.png"/></span>
<span><img @click="tubiao" style="cursor: pointer;" class="mr-10px h-30px w-30px"
src="@/assets/imgs/tubiao.png"/></span>
</el-form>
</div>

View File

@ -6,10 +6,9 @@
<ContentWrap>
<div class="container">
<div class="left" >
<div class="left">
<img :src="image"
style="width: 100%;height: 100%;border-radius: 10px;" v-show="check == 1" />
<img :src="image" style="width: 100%;height: 100%;border-radius: 10px;" v-show="check == 1" />
<span v-show="check == 2" style="margin-left:80px">暂无图片请添加~</span>
</div>
@ -27,23 +26,30 @@
<div v-for="(item, index) in timeRanges" :key="index" class="rectangle-container">
<div class="imageLeft">
<UploadImg width="100px" height="100px" v-model="item.pic" />
<UploadImg width="100px" height="100px" v-model="item.pic" />
</div>
<div class="inputRight">
<div style="margin-top: 3%;">
<span>图片名称</span><span><el-input style="width: 200px;" type="text" v-model="item.name"
placeholder="请输入名称" /></span>
<div style="margin-top: 20%;">
<span>图片名称</span>
</div>
<br />
<div>
<span>链接地址</span><span><el-input style="width: 200px;" type="text" v-model="item.url"
placeholder="请输入地址" /></span>
<span>链接地址</span>
</div>
</div>
<div class="top-right-image">
<img @click="removePic(index)" style="width: 25px;height: 25px;"
src="@/assets/imgs/quxiao.png" />
<div>
<div>
<el-input style="width: 200px;margin-top: 3%;" type="text" v-model="item.name"
placeholder="请输入名称" />
</div>
<br />
<div>
<AppLinkInput v-model="item.url" />
</div>
</div>
<img @click="removePic(index)"
style="width: 25px;height: 25px;position: absolute;top: 1px;right: 1px;cursor: pointer;"
src="@/assets/imgs/quxiao.png" />
<br />
</div>
@ -111,15 +117,15 @@
formData.value = await AdvertisingApi.getAdvertising()
if(formData.value.id == null){
console.log('11111:',formData.value)
}else{
console.log('22222:',formData.value)
if (formData.value.id == null) {
console.log('11111:', formData.value)
} else {
console.log('22222:', formData.value)
check.value = 1
timeRanges.value = formData.value.picData
if (formData.value.picData && formData.value.picData.length > 0) {
image.value = formData.value.picData[0].pic
}
image.value = formData.value.picData[0].pic
}
}
@ -160,13 +166,10 @@
height: 550px;
border-radius: 10px;
border: 1px solid #ddd;
/* background-color: lightblue; */
}
.right {
margin-left: 50px;
/* background-color: lightgreen; */
}
@ -174,37 +177,37 @@
* 上传图片
*/
.rectangle-container {
display: flex;
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
position: relative;
margin-top: 13px;
display: grid;
/* 使用 Grid 布局 */
grid-template-columns: 25% 20% 55%;
/* 设置四列,每列占 25% 宽度 */
/* 元素间的间隔 */
width: 450px;
/* 或者设置固定宽度 */
height: 100px;
}
.imageLeft {
/* .imageLeft {
width: 100px;
height: 100px;
margin-right: 20px;
} */
/* position: relative; */
}
.inputRight {
/* .inputRight {
display: flex;
flex-direction: column;
}
} */
/* .right-input {
background-color: aqua;
} */
.top-right-image {
position: absolute;
/* 使图片定位到父容器的右上角 */
top: 1px;
/* 控制图片与上边缘的距离 */
right: 1px;
/* 控制图片与右边缘的距离 */
cursor: pointer;
}
.image-preview {
width: 50px;

View File

@ -6,9 +6,13 @@
<span>客服类型</span>
<span>
<el-radio-group v-model="radios" @change="handleRadioChange">
<el-radio value="1" size="large">系统客服</el-radio>
<el-radio value="2" size="large">拨打电话</el-radio>
<!-- <el-radio value="3" size="large">跳转链接</el-radio> -->
<el-radio v-for="dict in getIntDictOptions(DICT_TYPE.KEFU_CONFIGURATION_TYPE)"
:key="dict.value"
:value="dict.value"
size="large"
>
{{dict.label}}
</el-radio>
</el-radio-group>
</span>
<p style="margin-left: 80px;color:#bcbaba;font-size:13px">系统客服点击联系客服使用系统的自带客服拨打电话点击联系客服拨打客服电话跳转链接跳转外部链接联系客服</p>
@ -76,6 +80,8 @@
<script setup lang="ts">
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { ConfigurationApi, ConfigurationVO } from '@/api/mall/promotion/configuration'
@ -84,7 +90,7 @@ const { t } = useI18n() // 国际化
const loading = ref(true) //
const radios = ref('1')
const radios = ref(1)
const formData = ref({
id: undefined,
@ -101,7 +107,7 @@ const getList = async () => {
loading.value = true
try {
formData.value = await ConfigurationApi.getConfiguration(Number(radios.value))
formData.value = await ConfigurationApi.getConfiguration(radios.value)
// radios.value = formData.value.id
console.log('11111',radios.value)

View File

@ -1,228 +0,0 @@
<template>
<ContentWrap>
<span>数据配置</span>
<el-button style="margin-left:85%">重置</el-button>
</ContentWrap>
<ContentWrap>
<div class="container">
<div class="left">
<img src="../../../kefu/components/asserts/aini.png" style="width: 100%;height: 100%;" />
</div>
<div class="right">
<div>
<p style="font-weight: 700;">引导页设置</p>
<span>开屏广告</span><span> <el-switch v-model="value" size="large" /> </span>
<br />
<span>广告时间</span><span> <el-input v-model="input" style="width: 240px" placeholder="请输入秒" /></span><span> 单位()</span>
<br />
</div>
<br/>
<div>
<!-- 动态生成多个长方体区域 -->
<div class="rectangle-container">
<div class="imageLeft">
<UploadImg width="100px" height="100px" v-model="pic" />
<!-- <img v-if="pic" :src="pic" alt="Uploaded image" class="image-preview" /> -->
</div>
<div class="inputRight">
<!-- 上下排列的输入框 -->
<input type="text" v-model="input1" placeholder="输入框 1" />
<input type="text" v-model="input2" placeholder="输入框 2" />
</div>
</div>
<!-- 添加按钮 -->
<el-button type="primary" @click="addNewItem">添加图片</el-button>
</div>
</div>
</div>
<div>
<p style="font-weight: 700;">引导页设置</p>
<span>开屏广告</span><span> <el-switch v-model="value" size="large" /> </span>
<br />
<span>广告时间</span><span> <el-input v-model="input" style="width: 240px" placeholder="请输入秒" /></span><span> 单位()</span>
<br />
</div>
<br/>
<div>
<div class="rectangle-container">
<div class="imageLeft">
<UploadImg width="100px" height="100px" v-model="pic" />
</div>
<div class="inputRight">
<input type="text" v-model="input1" placeholder="输入框 1" />
<input type="text" v-model="input2" placeholder="输入框 2" />
</div>
</div>
<el-button type="primary" @click="addNewItem">添加图片</el-button>
</div>
</ContentWrap>
</template>
<script>
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { AdvertisingApi, AdvertisingVO } from '@/api/mall/promotion/'
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
// const list = ref<AdvertisingVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
status: undefined,
time: [],
property: undefined,
createTime: []
})
const input1 = '0'
const input2 = ''
const items = [{id: '', image: '', input1: '', input2: '' }]
// const addNewItem = () => {
// items.push({id: '', image: '', input1: '', input2: '' });
// }
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AdvertisingApi.getAdvertisingPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
// /** / */
// const formRef = ref()
// const openForm = (type: string, id?: number) => {
// formRef.value.open(type, id)
// }
// /** **/
// onMounted(() => {
// getList()
// })
</script>
<style>
.container {
display: flex;
/* justify-content: space-between; /* 左右分布 */
width: 100%;
}
.left {
width: 300px;
height: 500px;
border-radius: 10px;
border: 1px solid #ddd;
border-radius: 8px;
/* background-color: lightblue; */
}
.right {
margin-left: 50px;
/* background-color: lightgreen; */
}
/**
* 上传图片
*/
.rectangle-container {
display: flex;
/* align-items: center;
justify-content: flex-start;
width: 100%;
margin-bottom: 20px; /* 给每个区域之间添加间隔 */
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
}
.imageLeft {
width: 100px;
height: 100px;
margin-right: 20px;
/* position: relative; */
}
/* .imageLeft input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 50%;
opacity: 0;
cursor: pointer;
} */
.image-preview {
width: 50px;
height: 50px;
/* object-fit: cover;
border-radius: 8px; */
}
.inputRight {
display: flex;
flex-direction: column;
}
.inputRight input {
margin-bottom: 10px;
padding: 8px;
width: 200px;
border: 1px solid #ccc;
border-radius: 4px;
}
/* button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
} */
/* button:hover {
background-color: #45a049;
} */
</style>

View File

@ -4,17 +4,17 @@
:show-tab-bar="selectedTemplateItem === 0" :show-navigation-bar="selectedTemplateItem !== 0"
:preview-url="previewUrl" @save="submitForm" @reset="handleEditorReset">
<template #toolBarLeft>
<!-- <el-radio-group
v-model="selectedTemplateItem"
class="h-full!"
@change="handleTemplateItemChange"
>
<el-tooltip v-for="(item, index) in templateItems" :key="index" :content="item.name">
<el-radio-button :label="index">
<Icon :icon="item.icon" :size="24" />
</el-radio-button>
</el-tooltip>
</el-radio-group> -->
<!-- <el-radio-group-->
<!-- v-model="selectedTemplateItem"-->
<!-- class="h-full!"-->
<!-- @change="handleTemplateItemChange"-->
<!-- >-->
<!-- <el-tooltip v-for="(item, index) in templateItems" :key="index" :content="item.name">-->
<!-- <el-radio-button :label="index">-->
<!-- <Icon :icon="item.icon" :size="24" />-->
<!-- </el-radio-button>-->
<!-- </el-tooltip>-->
<!-- </el-radio-group>-->
</template>
</DiyEditor>
</template>

View File

@ -89,8 +89,8 @@ const activeConversationId = ref(-1) // 选中的会话
const collapse = computed(() => appStore.getCollapse) //
/** 加载会话列表 */
const getConversationList = async () => {
const list = await KeFuConversationApi.getConversationListByKefuId(getStaffToken())
const getConversationList = async (name: string) => {
const list = await KeFuConversationApi.getConversationListByKefuId(getStaffToken(),name)
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
conversationList.value = list
}

View File

@ -113,7 +113,7 @@
</div>
</el-footer>
</el-container>
<el-empty v-else description="请选择左侧的一个会话后开始" />
<el-empty style="background-color: white;" v-else description="请选择左侧的一个会话后开始" />
<Dialog :title="dialogTitle" v-model="dialogVisible" :modal="false" width="700px">

View File

@ -2,11 +2,18 @@
<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 tab-position="top" v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="足迹" name="a" />
<el-tab-pane label="购买" name="b" />
</el-tabs>
<div>
<div >
<el-input @input="findgoodsNameInput" v-model="goodsName" placeholder="搜索商品名称" v-if="activeName === 'a'"
style="width: 100%;"/>
<el-input @input="findNoInput" v-model="no" placeholder="搜索订单编号" v-if="activeName === 'b'"
style="width: 100%;"/>
</div>
<div style="margin-top: 5px;">
<el-scrollbar ref="scrollbarRef" always height="calc(115vh - 400px)" @scroll="handleScroll">
<!-- 最近浏览 -->
<ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" />
@ -30,7 +37,8 @@ import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrol
defineOptions({ name: 'MemberBrowsingHistory' })
const activeName = ref('a')
const goodsName = ref('')
const no = ref('')
/** tab 切换 */
const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
@ -40,21 +48,32 @@ const handleClick = async (tab: TabsPaneContext) => {
await getHistoryList()
}
//
const findgoodsNameInput = async () => {
getHistoryList()
}
/** 获得历史数据 */
// TODO @puhui ab
const getHistoryList = async () => {
switch (activeName.value) {
case 'a':
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value,goodsName.value)
break
case 'b':
await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value,no.value)
break
default:
break
}
}
//
const findNoInput = async () => {
getHistoryList()
}
/** 加载下一页数据 */
const loadMore = async () => {
switch (activeName.value) {

View File

@ -0,0 +1,93 @@
<!-- 目录是不是叫 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-tab-pane label="未发货" name="c" />
<el-tab-pane label="退款中" name="d" />
</el-tabs>
<div>
<el-input @input="findNoInput" v-model="no" placeholder="搜索订单编号"
style="width: 100%;"/>
</div>
<div>
<el-scrollbar ref="scrollbarRef" always height="calc(115vh - 400px)" @scroll="handleScroll">
<OrderBrowsingHistorys ref="orderBrowsingHistorysRef" />
</el-scrollbar>
</div>
</div>
<el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
</template>
<script lang="ts" setup>
import type { TabsPaneContext } from 'element-plus'
import OrderBrowsingHistorys from './OrderBrowsingHistorys.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: 'MemberBrowsingHistorys' })
const activeName = ref('a')
/** tab 切换 */
const orderBrowsingHistorysRef = ref<InstanceType<typeof OrderBrowsingHistorys>>()
const handleClick = async (tab: TabsPaneContext) => {
activeName.value = tab.paneName as string
await nextTick()
await getHistoryList()
}
const no = ref('')
/** 获得历史数据 */
// TODO @puhui ab : abcd😈
const getHistoryList = async () => {
console.log('11111执行11111',activeName.value)
await orderBrowsingHistorysRef.value?.getHistoryList(conversation.value,activeName.value,no.value)
}
//
const findNoInput = async () => {
getHistoryList()
}
/** 加载下一页数据 */
const loadMore = async () => {
await orderBrowsingHistorysRef.value?.loadMore()
}
/** 浏览历史初始化 */
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>

View File

@ -8,14 +8,15 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getOrderPage } from '@/api/mall/trade/order'
import { concat } from 'lodash-es'
defineOptions({ name: 'OrderBrowsingHistory' })
defineOptions({ name: 'OrderBrowsingHistorys' })
const list = ref<any>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: 0
userId: 0,
no: '',
})
const skipGetMessageList = computed(() => {
//
@ -23,8 +24,9 @@ const skipGetMessageList = computed(() => {
}) //
/** 获得浏览记录 */
const getHistoryList = async (val: KeFuConversationRespVO) => {
const getHistoryList = async (val: KeFuConversationRespVO,no: string) => {
queryParams.userId = val.userId
queryParams.no = no
const res = await getOrderPage(queryParams)
total.value = res.total
list.value = res.list

View File

@ -0,0 +1,48 @@
<template>
<OrderItems v-for="item in list" :key="item.id" :order="item" class="mb-10px" />
</template>
<script lang="ts" setup>
import OrderItems from '@/views/mall/promotion/kefu/components/message/OrderItems.vue'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getOrderPage } from '@/api/mall/trade/order'
import { concat } from 'lodash-es'
defineOptions({ name: 'OrderBrowsingHistorys' })
const list = ref<any>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: 0,
activeName: 'a',
no: '',
})
const skipGetMessageList = computed(() => {
//
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
}) //
/** 获得浏览记录 */
const getHistoryList = async (val: KeFuConversationRespVO,activeName: string,no: string) => {
queryParams.userId = val.userId
queryParams.activeName = activeName
queryParams.no = no
const res = await getOrderPage(queryParams)
total.value = res.total
list.value = res.list
}
/** 加载下一页数据 */
const loadMore = async () => {
if (skipGetMessageList.value) {
return
}
queryParams.pageNo += 1
const res = await getOrderPage(queryParams)
total.value = res.total
concat(list.value, res.list)
}
defineExpose({ getHistoryList, loadMore })
</script>

View File

@ -27,7 +27,8 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
userId: 0,
userDeleted: false
userDeleted: false,
goodsName: ''
})
const skipGetMessageList = computed(() => {
//
@ -35,8 +36,9 @@ const skipGetMessageList = computed(() => {
}) //
/** 获得浏览记录 */
const getHistoryList = async (val: KeFuConversationRespVO) => {
const getHistoryList = async (val: KeFuConversationRespVO,goodsName: string) => {
queryParams.userId = val.userId
queryParams.goodsName = goodsName
const res = await getBrowseHistoryPage(queryParams)
total.value = res.total
list.value = res.list

View File

@ -1,6 +1,6 @@
import KeFuConversationList from './KeFuConversationList.vue'
import KeFuMessageList from './KeFuMessageList.vue'
import MemberBrowsingHistory from './history/MemberBrowsingHistory.vue'
import UserInfo from './UserInfo.vue'
import MemberBrowsingHistorys from './history/MemberBrowsingHistorys.vue'
export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory ,UserInfo}
export { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory ,MemberBrowsingHistorys}

View File

@ -0,0 +1,182 @@
<template>
<div v-if="isObject(getMessageContent)">
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
<div class="order-card-header flex items-center justify-between p-x-5px">
<div class="order-no">
订单号
<span style="cursor: pointer" @click="openDetail(getMessageContent.id)">
{{ getMessageContent.no }}
</span>
</div>
<div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
{{ formatOrderStatus(getMessageContent) }}
</div>
</div>
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
<ProductItem
:spu-id="item.spuId"
:num="item.count"
:picUrl="item.picUrl"
:price="item.price"
:skuText="item.properties.map((property: any) => property.valueName).join(' ')"
:title="item.spuName"
/>
</div>
<div class="pay-box flex justify-end pr-5px">
<div class="flex items-center">
<div class="discounts-title pay-color"
> {{ getMessageContent?.productCount }} 件商品,总金额:
</div>
<div class="discounts-money pay-color">
{{ fenToYuan(getMessageContent?.payPrice) }}
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { fenToYuan, jsonParse } from '@/utils'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { isObject } from '@/utils/is'
import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
const { push } = useRouter()
defineOptions({ name: 'OrderItems' })
const props = defineProps<{
message?: KeFuMessageRespVO
order?: any
}>()
const getMessageContent = computed(() =>
typeof props.message !== 'undefined' ? jsonParse(props!.message!.content) : props.order
)
/** 查看订单详情 */
const openDetail = (id: number) => {
console.log(getMessageContent)
push({ name: 'TradeOrderDetail', params: { id } })
}
/**
* 格式化订单状态的颜色
*
* @param order 订单
* @return {string} 颜色的 class 名称
*/
function formatOrderColor(order: any) {
if (order.status === 0) {
return 'info-color'
}
if (order.status === 10 || order.status === 20 || (order.status === 30 && !order.commentStatus)) {
return 'warning-color'
}
if (order.status === 30 && order.commentStatus) {
return 'success-color'
}
return 'danger-color'
}
/**
* 格式化订单状态
*
* @param order 订单
*/
function formatOrderStatus(order: any) {
if (order.status === 0) {
return '待付款'
}
if (order.status === 10 && order.deliveryType === 1) {
return '待发货'
}
if (order.status === 10 && order.deliveryType === 2) {
return '待核销'
}
if (order.status === 20) {
return '待收货'
}
if (order.status === 30 && !order.commentStatus) {
return '待评价'
}
if (order.status === 30 && order.commentStatus) {
return '已完成'
}
if (order.status === 100) {
return '退款中'
}
return '已关闭'
}
</script>
<style lang="scss" scoped>
.order-list-card-box {
border-radius: 10px;
padding: 10px;
border: 1px var(--el-border-color) solid;
background-color: var(--app-content-bg-color);
.order-card-header {
height: 28px;
.order-no {
font-size: 12px;
font-weight: 500;
span {
&:hover {
text-decoration: underline;
color: var(--left-menu-bg-active-color);
}
}
}
}
.pay-box {
padding-top: 10px;
.discounts-title {
font-size: 16px;
line-height: normal;
color: #999999;
}
.discounts-money {
font-size: 16px;
line-height: normal;
color: #999;
font-family: OPPOSANS;
}
.pay-color {
font-size: 13px;
color: var(--left-menu-text-color);
}
}
}
.warning-color {
color: #faad14;
font-size: 11px;
font-weight: bold;
}
.danger-color {
color: #ff3000;
font-size: 11px;
font-weight: bold;
}
.success-color {
color: #52c41a;
font-size: 11px;
font-weight: bold;
}
.info-color {
color: #999999;
font-size: 11px;
font-weight: bold;
}
</style>

View File

@ -3,7 +3,7 @@
<el-popover :width="500" placement="top" trigger="click">
<template #reference>
<!-- <Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" /> -->
<img :src="biaoqing" style="margin-left:20px" class="w-23px h-25px" />
<img :src="biaoqing" class="w-23px h-25px" />
</template>
<ElScrollbar height="300px">
<ul class="ml-2 flex flex-wrap px-2">

View File

@ -1,52 +1,57 @@
<template>
<div>
<!-- 新区域放在头部 -->
<el-row style="display: flex; justify-content: center;">
<el-row style="display: flex; justify-content: center;">
<el-col :span="24">
<div style="width:100%;height:68px;background-color:#3c80ff;">
<el-row>
<el-col :span="6">
<el-input
style="width: 80%;margin-top: 20px;margin-left:10px;"
:suffix-icon="Search"
/>
<el-input @input="findNameInput" v-model="findName"
style="width: 80%;margin-top: 20px;margin-left:23px;"
:suffix-icon="Search"/>
</el-col>
<el-col :span="1">
<el-avatar style=" margin-top: 15px;" :src="pic"/>
<!-- <span style="display: flex; margin-top: 15px;">
<span style="margin-left:5px;margin-top: 9px;">{{name}}</span>
<el-switch
style="margin-top: 4px;--el-switch-on-color: #13ce66; --el-switch-off-color: #b6bac1;"
v-model="lineStatus" class="ml-2" width="60" inline-prompt active-text="在线"
inactive-text="下线" @change="handleSwitchChange"/>
</span> -->
</el-col>
<el-col :span="7">
<span style="display: flex; margin-top: 15px;">
<!-- <el-avatar :src="pic"/> -->
<span style="margin-left:5px;margin-top: 9px;">{{name}}</span>
<el-switch
style="margin-top: 4px;--el-switch-on-color: #13ce66; --el-switch-off-color: #b6bac1;"
v-model="lineStatus" class="ml-2" width="60" inline-prompt active-text="在线"
inactive-text="下线" @change="handleSwitchChange"/>
</span>
</el-col>
<el-col :span="4">
<el-button @click="out" size="small" round
style="margin-top:23px;margin-left:72%">退出
</el-button>
</el-col>
<el-col :span="6">
<span style="display: flex; margin-top: 15px;">
<el-avatar
:src="pic"
/>
<span style="margin-left:5px;margin-top: 9px;">{{name}}</span>
<el-switch
style="margin-top: 4px;--el-switch-on-color: #13ce66; --el-switch-off-color: #b6bac1;"
v-model="lineStatus"
class="ml-2"
width="60"
inline-prompt
active-text="在线"
inactive-text="下线"
@change="handleSwitchChange"
/>
</span>
</el-col>
<el-col :span="6">
<el-button @click="out" size="small" round style="margin-top:23px;margin-left:75%">退出</el-button>
</el-col>
<el-col :span="6">
<el-menu
background-color="#3c80ff"
text-color="white"
active-text-color="white"
style="width:100%;display: flex;"
>
<el-menu-item @click="userInfo" style="width:33%;height:70px" index="1">客户信息</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>
<el-menu background-color="#3c80ff" text-color="white" active-text-color="white"
style="width:100%;display: flex;">
<el-menu-item @click="userInfo" style="width:33%;height:70px" index="1">客户信息
</el-menu-item>
<el-menu-item @click="jiaoyi" style="width:33%;height:70px" index="3">交易订单
</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>
</el-col>
</el-row>
@ -67,61 +72,77 @@
<!-- 会话详情选中会话的消息列表 -->
<el-col :span="12">
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList"/>
<KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList"/>
</el-col>
<!-- 会员足迹选中会话的会员足迹 -->
<el-col :span="6">
<ContentWrap v-show="chick == '2'">
<ContentWrap v-show="chick == '1' && clickUser == 2">
<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>
<ContentWrap v-show="chick == '1' && clickUser == 1">
<el-empty description="请选择左侧的一个会话后开始" />
</ContentWrap>
<ContentWrap v-show="chick == '2'">
<MemberBrowsingHistory ref="memberBrowsingHistoryRef"/>
</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 v-show="chick == '3'">
<MemberBrowsingHistorys ref="memberBrowsingHistorysRef"/>
</ContentWrap>
</el-col>
</el-row>
</div>
@ -129,21 +150,32 @@
<script lang="ts" setup>
import { KeFuConversationList, KeFuMessageList,MemberBrowsingHistory,UserInfo } from './components'
import {
KeFuConversationList,
KeFuMessageList,
MemberBrowsingHistorys,
MemberBrowsingHistory
} from './components'
import {WebSocketMessageTypeConstants} from './components/tools/constants'
import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import {KeFuConversationRespVO} from '@/api/mall/promotion/kefu/conversation'
import {getRefreshToken, getAccessToken} from '@/utils/auth'
import {useWebSocket} from '@vueuse/core'
import {Search} from '@element-plus/icons-vue'
import * as UserApi from '@/api/member/user'
import { SupportStaffApi, SupportStaffVO } from '@/api/mall/promotion/supportstaff'
import {SupportStaffApi, SupportStaffVO} from '@/api/mall/promotion/supportstaff'
import {string} from 'vue-types'
import type { TabsPaneContext } from 'element-plus'
const findName = ref('')
defineOptions({name: 'KeFu'})
const lineStatus = ref(true)
const clickUser = ref(1)
const message = useMessage() //
const params = new URLSearchParams(window.location.search);
@ -154,74 +186,91 @@
// const userInfoRef = ref<InstanceType<typeof UserInfo>>()
const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
const userId = ref(0)
const stat = ref(false)
// ======================= WebSocket start =======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken()
// 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket
const handleSwitchChange = async (value) =>{
console.log('11111:',value)
let a = 0;
if(value == true){
a = 1;
await SupportStaffApi.updateLineStatus(kefuId,a)
message.success('已上线')
}else{
a = 2;
await SupportStaffApi.updateLineStatus(kefuId,a)
message.success('已下线')
}
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' + getRefreshToken()
// getAccessToken()
// 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket
const handleSwitchChange = async (value) => {
console.log('11111:', value)
let a = 0;
if (value == true) {
a = 1;
await SupportStaffApi.updateLineStatus(kefuId, a)
message.success('已上线')
} else {
a = 2;
await SupportStaffApi.updateLineStatus(kefuId, a)
message.success('已下线')
}
let a = 0;
}
const {status, data, send, open, close} = useWebSocket(server.value, {
onConnected: function (ws) {
console.log('websocket 连接成功!', ws);
},
onDisconnected: function (ws, event) {
console.log('WebSocket 连接断开', event);
},
onError: function (ws, event) {
console.error('WebSocket 连接错误:', event);
if (event instanceof ErrorEvent) {
console.error('详细错误信息:', event.message);
} else {
console.error('非标准错误:', event);
}
},
onMessage: function (ws, event) {
console.log('收到的 WebSocket 消息:', event.data);
const shangxian = async () => {
await SupportStaffApi.updateLineStatus(kefuId, 1)
}
const xiaxian = async () => {
await SupportStaffApi.updateLineStatus(kefuId, 2)
}
let a = 0;
const {status, data, send, open, close} = useWebSocket(server.value, {
onConnected: function (ws) {
shangxian(); //线
console.log('websocket 连接成功!', ws);
},
onDisconnected: function (ws, event) {
xiaxian(); //线
console.log('WebSocket 连接断开', event);
},
// onError: function (ws, event) {
// console.error('WebSocket :', event);
// if (event instanceof ErrorEvent) {
// console.error(':', event.message);
// } else {
// console.error(':', event);
// }
// },
onMessage: function (ws, event) {
// console.log(' WebSocket :', event.data);
a = a + 1;
if (a == 2) {
getConversationList()
if(userId.value != 0){
getBySenderIdStat()
if(stat.value){
keFuChatBoxRef.value?.refreshMessageList()
}
}
a = 0;
}
a = a + 1 ;
if(a == 2){
getConversationList()
keFuChatBoxRef.value?.refreshMessageList()
a = 0;
}
},
autoReconnect: false, //
heartbeat: true
});
},
autoReconnect: true, //
heartbeat: true
});
// const { data, close, open } = useWebSocket(server.value, {
// autoReconnect: true,
// heartbeat: true
// })
/** 监听 WebSocket 数据 */
watchEffect(() => {
console.log('连接服务器得到消息:',data.value)
console.log('连接服务器得到消息:', data.value)
if (!data.value) {
return
}
@ -234,7 +283,7 @@
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
console.log('来自用户发送的消息:',data.value)
if (!type) {
message.error('未知的消息类型:' + data.value)
@ -242,6 +291,7 @@
}
// 2.2 KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
console.log('来自用户发送的消息:', JSON.parse(jsonMessage.content))
//
// TODO @puhui999 update
getConversationList()
@ -261,39 +311,63 @@
})
// ======================= WebSocket end =======================
/** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
const getConversationList = () => {
keFuConversationRef.value?.getConversationList()
keFuConversationRef.value?.getConversationList(findName.value)
}
/** 加载指定会话的消息列表 */
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
conversations.value = conversation
chick.value = '2'
userId.value = conversation.userId
const memberBrowsingHistorysRef = ref<InstanceType<typeof MemberBrowsingHistorys>>()
const handleChange = async (conversation: KeFuConversationRespVO) => {
conversations.value = conversation
// chick.value = '1'
clickUser.value = 2
userId.value = conversation.userId
userInfo()
keFuChatBoxRef.value?.getNewMessageList(conversation)
memberBrowsingHistoryRef.value?.initHistory(conversation)
memberBrowsingHistorysRef.value?.initHistory(conversation)
}
const out = () =>{
window.close();
// window.location.href = '/kefu/support-staff';
const out = async () => {
// await SupportStaffApi.updateLineStatus(kefuId, 2)
window.close();
// window.location.href = '/kefu/support-staff';
}
const getBySenderIdStat = async () => {
stat.value = await KeFuMessageApi.getBySenderIdStat(userId.value)
}
const chick = ref('1')
const userInfo = async () => {
chick.value = '1'
if(clickUser.value == 2){
user.value = await UserApi.getUserInfo(userId.value)
}
}
const zuoji = () => {
chick.value = '2'
// keFuChatBoxRef.value?.getNewMessageList(conversations.value)
memberBrowsingHistoryRef.value?.initHistory(conversations.value)
}
const jiaoyi = () => {
chick.value = '3'
// keFuChatBoxRef.value?.getNewMessageList(conversations.value)
// memberBrowsingHistoryRef.value?.initHistory(conversations.value)
}
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)
const findNameInput = () => {
keFuConversationRef.value?.getConversationList(findName.value)
}
/** 初始化 */
@ -301,21 +375,22 @@
getConversationList()
// websocket
open()
console.log('WebSocket 已初始化');
console.log('WebSocket 已初始化');
})
/** 销毁 */
onBeforeUnmount(() => {
// websocket
close()
console.log('WebSocket 已关闭');
})
// /** */
// onBeforeUnmount(() => {
// // websocket
// close()
// console.log('WebSocket ');
// })
</script>
<style lang="scss">
.kefu {
height: calc(100vh - 165px);
overflow: auto; /* 确保内容可滚动 */
overflow: auto;
/* 确保内容可滚动 */
}

View File

@ -246,7 +246,6 @@ const handleDelete = async (id: number) => {
}
/** 客服进入工作台 */
const handleEnterConsole = async (id: number,name: string,pic: string) => {
await SupportStaffApi.updateLineStatus(JSON.stringify(id),1)
setStaffToken(id);
const url = `${window.location.origin}/kefu/kefu?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}&pic=${encodeURIComponent(pic)}`;
window.open(url, '_blank');

View File

@ -41,10 +41,7 @@ public abstract class AbstractWebSocketMessageSender implements WebSocketMessage
send(sessionId, (Integer) null,null, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
send(sessionId, userType,userId, messageType, messageContent);
}
/**
* 发送消息

View File

@ -38,9 +38,6 @@ public interface WebSocketMessageSender {
void send(String sessionId, String messageType, String messageContent);
void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
@ -54,8 +51,6 @@ public interface WebSocketMessageSender {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
send(sessionId,userId,userType, messageType, JsonUtils.toJsonString(messageContent));
}
}

View File

@ -48,10 +48,7 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
sendRedisMessage(sessionId, null, null, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent) {
sendRedisMessage(sessionId, userId, userType, messageType, messageContent);
}
/**
* 通过 Redis 广播消息
@ -65,24 +62,6 @@ public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender
private void sendRedisMessage(String sessionId, Long userId, Integer userType,
String messageType, String messageContent) {
if (userType == 2){
ConcurrentMap<String, WebSocketSession> stringWebSocketSessionConcurrentMap = webSocketSessionManager.idSessions();
for (Map.Entry<String, WebSocketSession> entry : stringWebSocketSessionConcurrentMap.entrySet()) {
String key = entry.getKey();
WebSocketSession session = entry.getValue();
Map<String, Object> attributes = session.getAttributes();
LoginUser loginUser = (LoginUser) attributes.get("LOGIN_USER");
if(loginUser.getId() == 1 && loginUser.getUserType() == 2){
sessionId = key;
break;
}
// 处理每个 WebSocketSession
System.out.println("Key: " + key + ", Session: " + session);
}
}
RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
.setMessageType(messageType).setMessageContent(messageContent);

View File

@ -40,13 +40,12 @@ public interface WebSocketSenderApi {
void send(String sessionId, String messageType, String messageContent);
void send(String sessionId, Long userId, Integer userType,String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(Integer userType, String messageType, Object messageContent) { //用户发送消息
default void sendObject(Integer userType, String messageType, Object messageContent) {
send(userType, messageType, JsonUtils.toJsonString(messageContent));
}
@ -54,8 +53,5 @@ public interface WebSocketSenderApi {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(String sessionId, Long userId, Integer userType,String messageType, Object messageContent) {
send(sessionId,userId,userType, messageType, JsonUtils.toJsonString(messageContent));
}
}

View File

@ -31,9 +31,6 @@ public class WebSocketSenderApiImpl implements WebSocketSenderApi {
webSocketMessageSender.send(sessionId, messageType, messageContent);
}
@Override
public void send(String sessionId, Long userId, Integer userType, String messageType, String messageContent) {
webSocketMessageSender.send(sessionId, userId,userType,messageType, messageContent);
}
}

View File

@ -61,6 +61,22 @@ public class ProductBrowseHistoryController {
productBrowseHistoryRespVO.setIntroduction(productSpuDO.getIntroduction());
resultList.add(productBrowseHistoryRespVO);
}
if (pageReqVO.getGoodsName() != null){
List<ProductBrowseHistoryRespVO> results = new ArrayList<>();
for (ProductBrowseHistoryRespVO productBrowseHistoryRespVO : resultList) {
if (productBrowseHistoryRespVO.getSpuName().toLowerCase().contains(pageReqVO.getGoodsName().toLowerCase())) {
results.add(productBrowseHistoryRespVO);
}
}
PageResult<ProductBrowseHistoryRespVO> result = new PageResult<>();
result.setTotal((long)results.size());
result.setList(results);
return success(result);
}
PageResult<ProductBrowseHistoryRespVO> result = new PageResult<>();
result.setTotal(pageResult.getTotal());
result.setList(resultList);

View File

@ -30,4 +30,6 @@ public class ProductBrowseHistoryPageReqVO extends SortablePageParam {
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
private String goodsName;
}

View File

@ -91,6 +91,7 @@ public class AdvertisingController {
return success(advertisingService.saveAdvertising(createReqVO));
}
@GetMapping("/getAdvertising")
public CommonResult<AdvertisingRespVO> getAdvertising() {
AdvertisingDO advertising = advertisingService.getAdvertising();
@ -99,4 +100,5 @@ public class AdvertisingController {
}

View File

@ -16,7 +16,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
public class KeFuConfigurationPageReqVO extends PageParam {
@Schema(description = "客服类型", example = "1")
private String type;
private Integer type;
@Schema(description = "客服反馈")
private String feedback;

View File

@ -18,7 +18,7 @@ public class KeFuConfigurationRespVO {
@Schema(description = "客服类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty("客服类型")
private String type;
private Integer type;
@Schema(description = "客服反馈")
@ExcelProperty("客服反馈")

View File

@ -14,7 +14,7 @@ public class KeFuConfigurationSaveReqVO {
@Schema(description = "客服类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotEmpty(message = "客服类型不能为空")
private String type;
private Integer type;
@Schema(description = "客服反馈")
private String feedback;

View File

@ -20,6 +20,7 @@ import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@ -56,7 +57,7 @@ public class KeFuConversationController {
@GetMapping("/list")
@Operation(summary = "获得客服会话列表")
@PreAuthorize("@ss.hasPermission('promotion:kefu-conversation:query')")
public CommonResult<List<KeFuConversationRespVO>> getConversationList(@RequestParam(required = false, value = "kefuId") Long kefuId) {
public CommonResult<List<KeFuConversationRespVO>> getConversationList(@RequestParam(required = false, value = "kefuId") Long kefuId,String name) {
// 如果会话列表中的kefuId为null则转接给当前kefuId处理小程序新会话
if (kefuId != null) {
List<KeFuConversationDO> updateList = new ArrayList<>();
@ -80,8 +81,20 @@ public class KeFuConversationController {
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())));
return success(respList);
List<KeFuConversationRespVO> result = new ArrayList<>();
for (KeFuConversationRespVO keFuConversationRespVO : respList) {
if (keFuConversationRespVO.getUserNickname().toLowerCase().contains(name.toLowerCase())) {
result.add(keFuConversationRespVO);
}
}
return success(result);
}
@Operation(summary = "转接会话给指定客服")
@GetMapping("/transfer/{id}/{kefuId}")
public CommonResult<String> transferConversation(@PathVariable("id") Long id, @PathVariable("kefuId") Long kefuId) {

View File

@ -47,6 +47,11 @@ public class KeFuMessageController {
return success(messageService.sendKefuMessage(sendReqVO));
}
@GetMapping("/sendMess")
public String sendMess(String mess){
return messageService.sendMess(mess);
}
@PutMapping("/update-read-status")
@Operation(summary = "更新客服消息已读状态")
@Parameter(name = "conversationId", description = "会话编号", required = true)
@ -72,4 +77,11 @@ public class KeFuMessageController {
return success(result);
}
@GetMapping("/getBySenderIdStat")
@Operation(summary = "发送客服消息")
public CommonResult<Boolean> getLatestMessageBySenderId(Integer senderId) {
return success(messageService.getLatestMessageBySenderId(senderId));
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.promotion.controller.app.Advertising;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.advertising.vo.AdvertisingRespVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.advertising.AdvertisingDO;
import cn.iocoder.yudao.module.promotion.service.advertising.AdvertisingService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@RestController
@RequestMapping("/promotion/advertising")
public class AppAdvertisingController {
@Resource
private AdvertisingService advertisingService;
@GetMapping("/getAdvertising")
public CommonResult<AdvertisingRespVO> getAdvertising() {
AdvertisingDO advertising = advertisingService.getAppAdvertising();
return success(BeanUtils.toBean(advertising, AdvertisingRespVO.class));
}
}

View File

@ -30,7 +30,7 @@ public class KeFuConfigurationDO extends BaseDO {
/**
* 客服类型
*/
private String type;
private Integer type;
/**
* 客服反馈
*/

View File

@ -3,12 +3,11 @@ package cn.iocoder.yudao.module.promotion.dal.dataobject.kefu;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.promotion.enums.kefu.KeFuMessageContentTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.time.LocalDateTime;
/**
* 客服消息 DO
*
@ -86,4 +85,7 @@ public class KeFuMessageDO extends BaseDO {
@TableField(exist = false)
private String kefuName;
@TableField(exist = false)
private Integer stat;
}

View File

@ -51,4 +51,7 @@ public interface KeFuMessageMapper extends BaseMapperX<KeFuMessageDO> {
@Select(" SELECT avatar FROM system_users where id = #{id} ")
String findSystemUserAvatar(Long id);
@Select("SELECT read_status as stat FROM promotion_kefu_message WHERE sender_id = #{senderId} ORDER BY create_time DESC LIMIT 1")
String getLatestMessageBySenderId(Integer senderId);
}

View File

@ -43,6 +43,8 @@ public interface AdvertisingService {
*/
AdvertisingDO getAdvertising();
AdvertisingDO getAppAdvertising();
/**
* 获得开屏广告分页
*

View File

@ -82,6 +82,12 @@ public class AdvertisingServiceImpl implements AdvertisingService {
return advertisingDO;
}
@Override
public AdvertisingDO getAppAdvertising() {
List<AdvertisingDO> advertisingDOS = advertisingMapper.selectList();
return advertisingDOS.get(0);
}
@Override
public PageResult<AdvertisingDO> getAdvertisingPage(AdvertisingPageReqVO pageReqVO) {
return advertisingMapper.selectPage(pageReqVO);

View File

@ -40,6 +40,7 @@ public class KeFuConfigurationServiceImpl implements KeFuConfigurationService {
@Override
public void updateKeFuConfiguration(KeFuConfigurationSaveReqVO updateReqVO) {
// 校验存在
validateKeFuConfigurationExists(updateReqVO.getId());
// 更新

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuM
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
import javax.validation.Valid;
import java.util.Optional;
/**
* 客服消息 Service 接口
@ -24,6 +25,8 @@ public interface KeFuMessageService {
*/
Long sendKefuMessage(@Valid KeFuMessageSendReqVO sendReqVO);
String sendMess(String mess);
/**
* 会员发送客服消息
*
@ -63,4 +66,11 @@ public interface KeFuMessageService {
String findSystemUserAvatar(Long id);
/**
* 根据 senderId 查询创建时间最近的一条数据
* @param senderId 发送人编号
* @return Optional<KeFuMessageDO> 返回最近的一条消息
*/
Boolean getLatestMessageBySenderId(Integer senderId);
}

View File

@ -15,13 +15,20 @@ import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMe
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.configuration.KeFuConfigurationDO;
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.supportstaff.SupportStaffDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.configuration.KeFuConfigurationMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuConversationMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuMessageMapper;
import cn.iocoder.yudao.module.promotion.dal.mysql.supportstaff.SupportStaffMapper;
import cn.iocoder.yudao.module.promotion.enums.kefu.KeFuMessageContentTypeEnum;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyMessageDO;
import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyMessageMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -30,6 +37,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -58,9 +66,15 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
@Resource
private WebSocketSenderApi webSocketSenderApi;
@Resource
private SupportStaffMapper supportStaffMapper;
@Resource
private NotifyMessageMapper notifyMessageMapper;
@Resource
private KeFuConfigurationMapper keFuConfigurationMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long sendKefuMessage(KeFuMessageSendReqVO sendReqVO) { //客服发消息
@ -83,6 +97,12 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
return kefuMessage.getId();
}
@Override
public String sendMess(String mess) {
webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), 1L, KEFU_MESSAGE_TYPE, mess);
return "发送成功";
}
@Override
public Long sendKefuMessage(AppKeFuMessageSendReqVO sendReqVO) { //用户发消息
// 1.1 设置会话编号
@ -96,11 +116,33 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
// 2. 更新会话消息冗余
conversationService.updateConversationLastMessage(kefuMessage);
getSelf().sendAsyncMessageToMembers(conversation.getKefuId(), KEFU_MESSAGE_TYPE, kefuMessage);
//客服实时接收消息
// getSelf().sendAsyncMessageToMembers(conversation.getKefuId(), KEFU_MESSAGE_TYPE, kefuMessage);
//判断客服是否在线,自动回复
SupportStaffDO supportStaffDO = supportStaffMapper.selectOne("id", conversation.getKefuId());
if (supportStaffDO.getLineStatus() == 2){ //离线
KeFuConfigurationDO type = keFuConfigurationMapper.selectOne("type", 1);
if (type != null){ //是否配置了客服不在线回复
if (type.getFeedback() != null){
KeFuMessageDO keFuMessageDO = new KeFuMessageDO();
keFuMessageDO.setSenderId(conversation.getKefuId());
keFuMessageDO.setSenderType(UserTypeEnum.ADMIN.getValue());
keFuMessageDO.setContent(type.getFeedback());
keFuMessageDO.setContentType(KeFuMessageContentTypeEnum.TEXT.getType());
keFuMessageDO.setConversationId(conversation.getId());
keFuMessageDO.setReceiverId(conversation.getUserId()).setReceiverType(UserTypeEnum.MEMBER.getValue()); // 设置接收人
keFuMessageMapper.insert(keFuMessageDO);
// 2.2 更新会话消息冗余
conversationService.updateConversationLastMessage(keFuMessageDO);
// 3.1 发送消息给用户
getSelf().sendAsyncMessageToMember(conversation.getUserId(), KEFU_MESSAGE_TYPE, keFuMessageDO);
}
}
}
// 3. 通知所有管理员更新对话
getSelf().sendAsyncMessageToAdmins(KEFU_MESSAGE_TYPE, kefuMessage);
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, kefuMessage);
return kefuMessage.getId();
}
//添加站内信
@ -169,7 +211,7 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
@Async
public void sendAsyncMessageToMembers(Long userId, String messageType, Object content) {
webSocketSenderApi.sendObject("",userId, UserTypeEnum.ADMIN.getValue(), messageType, content);
webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId, messageType, content);
}
@Async
@ -177,10 +219,6 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), messageType, content);
}
@Async
public void sendAsyncMessageToAdmins(String messageType, Object content) {
webSocketSenderApi.sendObject(UserTypeEnum.MEMBER.getValue(), messageType, content);
}
@Override
public PageResult<KeFuMessageDO> getKeFuMessagePage(KeFuMessagePageReqVO pageReqVO) {
@ -204,6 +242,15 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
return keFuMessageMapper.findSystemUserAvatar(id);
}
@Override
public Boolean getLatestMessageBySenderId(Integer senderId) {
String stat = keFuMessageMapper.getLatestMessageBySenderId(senderId);
if (stat.equals("0")){ //未读:0, 已读1
return true;
}
return false;
}
private KeFuMessageServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}

View File

@ -52,6 +52,8 @@ public class TradeOrderController {
@Operation(summary = "获得交易订单分页")
@PreAuthorize("@ss.hasPermission('trade:order:query')")
public CommonResult<PageResult<TradeOrderPageItemRespVO>> getOrderPage(TradeOrderPageReqVO reqVO) {
// 查询订单
PageResult<TradeOrderDO> pageResult = tradeOrderQueryService.getOrderPage(reqVO);
if (CollUtil.isEmpty(pageResult.getList())) {

View File

@ -61,4 +61,7 @@ public class TradeOrderPageReqVO extends PageParam {
@InEnum(value = TerminalEnum.class, message = "订单来源 {value}")
private Integer terminal;
//客服工作台用户交易订单类型
private String activeName;
}

View File

@ -6,10 +6,14 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.trade.controller.admin.aftersale.vo.AfterSalePageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@Mapper
public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
@ -48,4 +52,9 @@ public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
.in(AfterSaleDO::getStatus, statuses));
}
@Select(" SELECT a.* FROM trade_order a " +
" INNER JOIN trade_after_sale b on a.id = b.order_id " +
" where a.user_id = #{userId} and b.status != 50 LIMIT #{startRow},#{limit} ")
List<TradeOrderDO> traderOrderList(@Param("userId") Long userId,@Param("startRow") Integer startRow, @Param("limit") Integer limit);
}

View File

@ -12,9 +12,11 @@ import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderPageReq
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderSummaryRespVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppPointOrderVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.mysql.aftersale.AfterSaleMapper;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderItemMapper;
import cn.iocoder.yudao.module.trade.dal.mysql.order.TradeOrderMapper;
import cn.iocoder.yudao.module.trade.dal.redis.RedisKeyConstants;
@ -57,6 +59,9 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
@Resource
private MemberUserApi memberUserApi;
@Resource
private AfterSaleMapper afterSaleMapper;
// =================== Order ===================
@Override
@ -89,6 +94,31 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
@Override
public PageResult<TradeOrderDO> getOrderPage(TradeOrderPageReqVO reqVO) {
if(reqVO.getActiveName() != null){
if (reqVO.getActiveName().equals("b")){ //未付款
reqVO.setStatus(TradeOrderStatusEnum.UNPAID.getStatus());
}
if (reqVO.getActiveName().equals("c")){ //未发货
reqVO.setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus());
}
if (reqVO.getActiveName().equals("d")){ //退款中
Integer startRow = (reqVO.getPageNo() - 1) * reqVO.getPageSize();
List<TradeOrderDO> tradeOrderDOS = afterSaleMapper.traderOrderList(reqVO.getUserId(), startRow, reqVO.getPageSize());
for (int i = 0; i < tradeOrderDOS.size(); i++) {
tradeOrderDOS.get(i).setStatus(100);
}
// 根据用户查询条件构建用户编号列表
Set<Long> userIds = buildQueryConditionUserIds(reqVO);
if (userIds == null) { // 没查询到用户说明肯定也没他的订单
return PageResult.empty();
}
// 分页查询
PageResult<TradeOrderDO> tradeOrderDOPageResult = new PageResult<>();
tradeOrderDOPageResult.setList(tradeOrderDOS);
tradeOrderDOPageResult.setTotal((long)tradeOrderDOS.size());
return tradeOrderDOPageResult;
}
}
// 根据用户查询条件构建用户编号列表
Set<Long> userIds = buildQueryConditionUserIds(reqVO);
if (userIds == null) { // 没查询到用户说明肯定也没他的订单