Skip to content

Commit 4abd246

Browse files
YunaiVgitee-org
authored andcommitted
!483 完善 mall 客服
Merge pull request !483 from puhui999/dev-crm
2 parents ccf21dd + d072098 commit 4abd246

15 files changed

+504
-350
lines changed

src/api/mall/product/history.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import request from '@/config/axios'
2+
3+
/**
4+
* 获得商品浏览记录分页
5+
* @param params 请求参数
6+
*/
7+
export const getBrowseHistoryPage = (params: any) => {
8+
return request.get({ url: '/product/browse-history/page', params })
9+
}

src/views/mall/promotion/kefu/components/KeFuConversationList.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
</div>
2222
<div class="ml-10px w-100%">
2323
<div class="flex justify-between items-center w-100%">
24-
<span>{{ item.userNickname }}</span>
24+
<span class="username">{{ item.userNickname }}</span>
2525
<span class="color-[#989EA6]">
26-
{{ formatDate(item.lastMessageTime) }}
26+
{{ formatPast(item.lastMessageTime, 'YYYY-mm-dd') }}
2727
</span>
2828
</div>
2929
<!-- 最后聊天内容 -->
@@ -70,7 +70,7 @@
7070
<script lang="ts" setup>
7171
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
7272
import { useEmoji } from './tools/emoji'
73-
import { formatDate } from '@/utils/formatTime'
73+
import { formatPast } from '@/utils/formatTime'
7474
import { KeFuMessageContentTypeEnum } from './tools/constants'
7575
import { useAppStore } from '@/store/modules/app'
7676
@@ -185,6 +185,16 @@ watch(showRightMenu, (val) => {
185185
background-color: #fff;
186186
transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
187187
188+
.username {
189+
min-width: 0;
190+
max-width: 60%;
191+
overflow: hidden;
192+
text-overflow: ellipsis;
193+
display: -webkit-box;
194+
-webkit-box-orient: vertical;
195+
-webkit-line-clamp: 1;
196+
}
197+
188198
.last-message {
189199
width: 200px;
190200
overflow: hidden; // 隐藏超出的文本

src/views/mall/promotion/kefu/components/KeFuMessageList.vue

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,54 @@
4040
v-if="item.senderType === UserTypeEnum.MEMBER"
4141
:src="conversation.userAvatar"
4242
alt="avatar"
43+
class="w-60px h-60px"
4344
/>
4445
<div
4546
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
4647
class="p-10px"
4748
>
4849
<!-- 文本消息 -->
49-
<TextMessageItem :message="item" />
50+
<MessageItem :message="item">
51+
<template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
52+
<div
53+
v-dompurify-html="replaceEmoji(item.content)"
54+
class="flex items-center"
55+
></div>
56+
</template>
57+
</MessageItem>
5058
<!-- 图片消息 -->
51-
<ImageMessageItem :message="item" />
59+
<MessageItem :message="item">
60+
<el-image
61+
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
62+
:initial-index="0"
63+
:preview-src-list="[item.content]"
64+
:src="item.content"
65+
class="w-200px"
66+
fit="contain"
67+
preview-teleported
68+
/>
69+
</MessageItem>
5270
<!-- 商品消息 -->
53-
<ProductMessageItem :message="item" />
71+
<MessageItem :message="item">
72+
<ProductItem
73+
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
74+
:picUrl="getMessageContent(item).picUrl"
75+
:price="getMessageContent(item).price"
76+
:skuText="getMessageContent(item).introduction"
77+
:title="getMessageContent(item).spuName"
78+
:titleWidth="400"
79+
class="max-w-70%"
80+
priceColor="#FF3000"
81+
/>
82+
</MessageItem>
5483
<!-- 订单消息 -->
55-
<OrderMessageItem :message="item" />
84+
<MessageItem :message="item">
85+
<OrderItem
86+
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
87+
:message="item"
88+
class="max-w-70%"
89+
/>
90+
</MessageItem>
5691
</div>
5792
<el-avatar
5893
v-if="item.senderType === UserTypeEnum.ADMIN"
@@ -97,24 +132,24 @@ import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/mes
97132
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
98133
import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
99134
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
100-
import TextMessageItem from './message/TextMessageItem.vue'
101-
import ImageMessageItem from './message/ImageMessageItem.vue'
102-
import ProductMessageItem from './message/ProductMessageItem.vue'
103-
import OrderMessageItem from './message/OrderMessageItem.vue'
104-
import { Emoji } from './tools/emoji'
135+
import ProductItem from './message/ProductItem.vue'
136+
import OrderItem from './message/OrderItem.vue'
137+
import { Emoji, useEmoji } from './tools/emoji'
105138
import { KeFuMessageContentTypeEnum } from './tools/constants'
106139
import { isEmpty } from '@/utils/is'
107140
import { UserTypeEnum } from '@/utils/constants'
108141
import { formatDate } from '@/utils/formatTime'
109142
import dayjs from 'dayjs'
110143
import relativeTime from 'dayjs/plugin/relativeTime'
144+
import { debounce } from 'lodash-es'
145+
import { jsonParse } from '@/utils'
111146
112147
dayjs.extend(relativeTime)
113148
114149
defineOptions({ name: 'KeFuMessageList' })
115150
116151
const message = ref('') // 消息弹窗
117-
152+
const { replaceEmoji } = useEmoji()
118153
const messageTool = useMessage()
119154
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
120155
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
@@ -126,18 +161,10 @@ const queryParams = reactive({
126161
})
127162
const total = ref(0) // 消息总条数
128163
const refreshContent = ref(false) // 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
164+
/** 获悉消息内容 */
165+
const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
129166
/** 获得消息列表 */
130-
const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
131-
// 会话切换,重置相关参数
132-
if (conversationChange) {
133-
queryParams.pageNo = 1
134-
messageList.value = []
135-
total.value = 0
136-
loadHistory.value = false
137-
refreshContent.value = false
138-
}
139-
conversation.value = val
140-
queryParams.conversationId = val.id
167+
const getMessageList = async () => {
141168
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
142169
total.value = res.total
143170
// 情况一:加载最新消息
@@ -146,14 +173,17 @@ const getMessageList = async (val: KeFuConversationRespVO, conversationChange: b
146173
} else {
147174
// 情况二:加载历史消息
148175
for (const item of res.list) {
149-
if (messageList.value.some((val) => val.id === item.id)) {
150-
continue
151-
}
152-
messageList.value.push(item)
176+
pushMessage(item)
153177
}
154178
}
155179
refreshContent.value = true
156-
await scrollToBottom()
180+
}
181+
/** 添加消息 */
182+
const pushMessage = (message: any) => {
183+
if (messageList.value.some((val) => val.id === message.id)) {
184+
return
185+
}
186+
messageList.value.push(message)
157187
}
158188
159189
/** 按照时间倒序,获取消息列表 */
@@ -163,20 +193,44 @@ const getMessageList0 = computed(() => {
163193
})
164194
165195
/** 刷新消息列表 */
166-
const refreshMessageList = async () => {
196+
const refreshMessageList = async (message?: any) => {
167197
if (!conversation.value) {
168198
return
169199
}
170200
171-
queryParams.pageNo = 1
172-
await getMessageList(conversation.value, false)
201+
if (typeof message !== 'undefined') {
202+
// 当前查询会话与消息所属会话不一致则不做处理
203+
if (message.conversationId !== conversation.value.id) {
204+
return
205+
}
206+
pushMessage(message)
207+
} else {
208+
queryParams.pageNo = 1
209+
await getMessageList()
210+
}
211+
173212
if (loadHistory.value) {
174213
// 右下角显示有新消息提示
175214
showNewMessageTip.value = true
215+
} else {
216+
// 滚动到最新消息处
217+
await handleToNewMessage()
176218
}
177219
}
178-
179-
defineExpose({ getMessageList, refreshMessageList })
220+
const getNewMessageList = async (val: KeFuConversationRespVO) => {
221+
// 会话切换,重置相关参数
222+
queryParams.pageNo = 1
223+
messageList.value = []
224+
total.value = 0
225+
loadHistory.value = false
226+
refreshContent.value = false
227+
// 设置会话相关属性
228+
conversation.value = val
229+
queryParams.conversationId = val.id
230+
// 获取消息
231+
await refreshMessageList()
232+
}
233+
defineExpose({ getNewMessageList, refreshMessageList })
180234
const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 是否显示聊天区域
181235
const skipGetMessageList = computed(() => {
182236
// 已加载到最后一页的话则不触发新的消息获取
@@ -221,9 +275,7 @@ const sendMessage = async (msg: any) => {
221275
await KeFuMessageApi.sendKeFuMessage(msg)
222276
message.value = ''
223277
// 加载消息列表
224-
await getMessageList(conversation.value, false)
225-
// 滚动到最新消息处
226-
await scrollToBottom()
278+
await refreshMessageList()
227279
}
228280
229281
/** 滚动到底部 */
@@ -248,17 +300,24 @@ const handleToNewMessage = async () => {
248300
await scrollToBottom()
249301
}
250302
251-
/** 加载历史消息 */
252303
const loadHistory = ref(false) // 加载历史消息
253-
const handleScroll = async ({ scrollTop }) => {
304+
/** 处理消息列表滚动事件(debounce 限流) */
305+
const handleScroll = debounce(({ scrollTop }) => {
254306
if (skipGetMessageList.value) {
255307
return
256308
}
257309
// 触顶自动加载下一页数据
258-
if (scrollTop === 0) {
259-
await handleOldMessage()
310+
if (Math.floor(scrollTop) === 0) {
311+
handleOldMessage()
260312
}
261-
}
313+
const wrap = scrollbarRef.value?.wrapRef
314+
// 触底重置
315+
if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
316+
loadHistory.value = false
317+
refreshMessageList()
318+
}
319+
}, 200)
320+
/** 加载历史消息 */
262321
const handleOldMessage = async () => {
263322
// 记录已有页面高度
264323
const oldPageHeight = innerRef.value?.clientHeight
@@ -268,7 +327,7 @@ const handleOldMessage = async () => {
268327
loadHistory.value = true
269328
// 加载消息列表
270329
queryParams.pageNo += 1
271-
await getMessageList(conversation.value, false)
330+
await getMessageList()
272331
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
273332
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
274333
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<template>
2+
<div v-show="!isEmpty(conversation)" class="kefu">
3+
<div class="header-title h-60px flex justify-center items-center">他的足迹</div>
4+
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
5+
<el-tab-pane label="最近浏览" name="a" />
6+
<el-tab-pane label="订单列表" name="b" />
7+
</el-tabs>
8+
<div>
9+
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 400px)" @scroll="handleScroll">
10+
<!-- 最近浏览 -->
11+
<ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" />
12+
<!-- 订单列表 -->
13+
<OrderBrowsingHistory v-if="activeName === 'b'" ref="orderBrowsingHistoryRef" />
14+
</el-scrollbar>
15+
</div>
16+
</div>
17+
<el-empty v-show="isEmpty(conversation)" description="请选择左侧的一个会话后开始" />
18+
</template>
19+
20+
<script lang="ts" setup>
21+
import type { TabsPaneContext } from 'element-plus'
22+
import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
23+
import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
24+
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
25+
import { isEmpty } from '@/utils/is'
26+
import { debounce } from 'lodash-es'
27+
import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar'
28+
29+
defineOptions({ name: 'MemberBrowsingHistory' })
30+
31+
const activeName = ref('a')
32+
/** tab 切换 */
33+
const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
34+
const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
35+
const handleClick = async (tab: TabsPaneContext) => {
36+
activeName.value = tab.paneName as string
37+
await nextTick()
38+
await getHistoryList()
39+
}
40+
/** 获得历史数据 */
41+
const getHistoryList = async () => {
42+
switch (activeName.value) {
43+
case 'a':
44+
await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
45+
break
46+
case 'b':
47+
await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
48+
break
49+
default:
50+
break
51+
}
52+
}
53+
/** 加载下一页数据 */
54+
const loadMore = async () => {
55+
switch (activeName.value) {
56+
case 'a':
57+
await productBrowsingHistoryRef.value?.loadMore()
58+
break
59+
case 'b':
60+
await orderBrowsingHistoryRef.value?.loadMore()
61+
break
62+
default:
63+
break
64+
}
65+
}
66+
/** 浏览历史初始化 */
67+
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
68+
const initHistory = async (val: KeFuConversationRespVO) => {
69+
activeName.value = 'a'
70+
conversation.value = val
71+
await nextTick()
72+
await getHistoryList()
73+
}
74+
defineExpose({ initHistory })
75+
76+
/** 处理消息列表滚动事件(debounce 限流) */
77+
const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
78+
const handleScroll = debounce(() => {
79+
const wrap = scrollbarRef.value?.wrapRef
80+
// 触底重置
81+
if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
82+
loadMore()
83+
}
84+
}, 200)
85+
</script>
86+
87+
<style lang="scss" scoped>
88+
.header-title {
89+
border-bottom: #e4e0e0 solid 1px;
90+
}
91+
</style>

0 commit comments

Comments
 (0)