Skip to content

Commit cabfc5c

Browse files
YunaiVgitee-org
authored andcommitted
!470 【新增】:mall 客服选择并发送图片信息
Merge pull request !470 from puhui999/dev-crm
2 parents edaf30a + 2b329d3 commit cabfc5c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+405
-175
lines changed

.env.local

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,5 @@ VITE_BASE_PATH=/
2929
# 商城H5会员端域名
3030
VITE_MALL_H5_DOMAIN='http://localhost:3000'
3131

32-
# TODO puhui999:这个可以不走 cdn 地址么?
33-
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
34-
VITE_STATIC_URL = https://file.sheepjs.com
35-
3632
# 验证码的开关
3733
VITE_APP_CAPTCHA_ENABLE=false

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

Lines changed: 110 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,75 +6,69 @@
66
<el-main class="kefu-content" style="overflow: visible">
77
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)">
88
<div ref="innerRef" class="w-[100%] pb-3px">
9-
<div
10-
v-for="item in messageList"
11-
:key="item.id"
12-
:class="[
13-
item.senderType === UserTypeEnum.MEMBER
14-
? `ss-row-left`
15-
: item.senderType === UserTypeEnum.ADMIN
16-
? `ss-row-right`
17-
: ''
18-
]"
19-
class="flex mb-20px w-[100%]"
20-
>
21-
<el-avatar
22-
v-show="item.senderType === UserTypeEnum.MEMBER"
23-
:src="keFuConversation.userAvatar"
24-
alt="avatar"
25-
/>
26-
<div class="kefu-message p-10px">
27-
<!-- TODO puhui999: 消息相关等后续完成后统一抽离封装 -->
28-
<!-- 文本消息 -->
29-
<template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
30-
<div
31-
v-dompurify-html="replaceEmoji(item.content)"
32-
:class="[
33-
item.senderType === UserTypeEnum.MEMBER
34-
? `ml-10px`
35-
: item.senderType === UserTypeEnum.ADMIN
36-
? `mr-10px`
37-
: ''
38-
]"
39-
class="flex items-center"
40-
></div>
41-
</template>
42-
<!-- 图片消息 -->
43-
<template v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType">
44-
<div
45-
:class="[
46-
item.senderType === UserTypeEnum.MEMBER
47-
? `ml-10px`
48-
: item.senderType === UserTypeEnum.ADMIN
49-
? `mr-10px`
50-
: ''
51-
]"
52-
class="flex items-center"
53-
>
54-
<el-image
55-
:src="item.content"
56-
fit="contain"
57-
style="width: 200px; height: 200px"
58-
@click="imagePreview(item.content)"
59-
/>
60-
</div>
61-
</template>
9+
<div v-for="(item, index) in messageList" :key="item.id" class="w-[100%]">
10+
<div class="flex justify-center items-center mb-20px">
11+
<!-- 日期 -->
12+
<div
13+
v-if="
14+
item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)
15+
"
16+
class="date-message"
17+
>
18+
{{ formatDate(item.createTime) }}
19+
</div>
20+
<!-- 系统消息 -->
21+
<view
22+
v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
23+
class="system-message"
24+
>
25+
{{ item.content }}
26+
</view>
27+
</div>
28+
<div
29+
:class="[
30+
item.senderType === UserTypeEnum.MEMBER
31+
? `ss-row-left`
32+
: item.senderType === UserTypeEnum.ADMIN
33+
? `ss-row-right`
34+
: ''
35+
]"
36+
class="flex mb-20px w-[100%]"
37+
>
38+
<el-avatar
39+
v-if="item.senderType === UserTypeEnum.MEMBER"
40+
:src="keFuConversation.userAvatar"
41+
alt="avatar"
42+
/>
43+
<div
44+
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
45+
class="p-10px"
46+
>
47+
<!-- 文本消息 -->
48+
<TextMessageItem :message="item" />
49+
<!-- 图片消息 -->
50+
<ImageMessageItem :message="item" />
51+
</div>
52+
<el-avatar
53+
v-if="item.senderType === UserTypeEnum.ADMIN"
54+
:src="item.senderAvatar"
55+
alt="avatar"
56+
/>
6257
</div>
63-
<el-avatar
64-
v-show="item.senderType === UserTypeEnum.ADMIN"
65-
:src="item.senderAvatar"
66-
alt="avatar"
67-
/>
6858
</div>
6959
</div>
7060
</el-scrollbar>
7161
</el-main>
7262
<el-footer height="230px">
7363
<div class="h-[100%]">
74-
<div class="chat-tools">
64+
<div class="chat-tools flex items-center">
7565
<EmojiSelectPopover @select-emoji="handleEmojiSelect" />
66+
<PictureSelectUpload
67+
class="ml-15px mt-3px cursor-pointer"
68+
@send-picture="handleSendPicture"
69+
/>
7670
</div>
77-
<el-input v-model="message" :rows="6" type="textarea" />
71+
<el-input v-model="message" :rows="6" style="border-style: none" type="textarea" />
7872
<div class="h-45px flex justify-end">
7973
<el-button class="mt-10px" type="primary" @click="handleSendMessage">发送</el-button>
8074
</div>
@@ -88,19 +82,25 @@
8882
import { ElScrollbar as ElScrollbarType } from 'element-plus'
8983
import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
9084
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
91-
import EmojiSelectPopover from './EmojiSelectPopover.vue'
92-
import { Emoji, replaceEmoji } from './emoji'
93-
import { KeFuMessageContentTypeEnum } from './constants'
85+
import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
86+
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
87+
import TextMessageItem from './message/TextMessageItem.vue'
88+
import ImageMessageItem from './message/ImageMessageItem.vue'
89+
import { Emoji } from './tools/emoji'
90+
import { KeFuMessageContentTypeEnum } from './tools/constants'
9491
import { isEmpty } from '@/utils/is'
9592
import { UserTypeEnum } from '@/utils/constants'
96-
import { createImageViewer } from '@/components/ImageViewer'
93+
import { formatDate } from '@/utils/formatTime'
94+
import dayjs from 'dayjs'
95+
import relativeTime from 'dayjs/plugin/relativeTime'
96+
97+
dayjs.extend(relativeTime)
9798
9899
defineOptions({ name: 'KeFuMessageBox' })
99100
const messageTool = useMessage()
100101
const message = ref('') // 消息
101102
const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
102103
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
103-
const poller = ref<any>(null) // TODO puhui999: 轮训定时器,暂时模拟 websocket
104104
// 获得消息 TODO puhui999: 先不考虑下拉加载历史消息
105105
const getMessageList = async (conversation: KeFuConversationRespVO) => {
106106
keFuConversation.value = conversation
@@ -111,32 +111,49 @@ const getMessageList = async (conversation: KeFuConversationRespVO) => {
111111
messageList.value = list.reverse()
112112
// TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
113113
await scrollToBottom()
114-
// TODO puhui999: 轮训相关,功能完善后移除
115-
if (!poller.value) {
116-
poller.value = setInterval(() => {
117-
getMessageList(conversation)
118-
}, 1000)
114+
}
115+
// 刷新消息列表
116+
const refreshMessageList = () => {
117+
if (!keFuConversation.value) {
118+
return
119119
}
120+
getMessageList(keFuConversation.value)
120121
}
121-
defineExpose({ getMessageList })
122+
defineExpose({ getMessageList, refreshMessageList })
122123
// 是否显示聊天区域
123124
const showChatBox = computed(() => !isEmpty(keFuConversation.value))
124125
// 处理表情选择
125126
const handleEmojiSelect = (item: Emoji) => {
126127
message.value += item.name
127128
}
129+
// 处理图片发送
130+
const handleSendPicture = async (picUrl: string) => {
131+
// 组织发送消息
132+
const msg = {
133+
conversationId: keFuConversation.value.id,
134+
contentType: KeFuMessageContentTypeEnum.IMAGE,
135+
content: picUrl
136+
}
137+
await sendMessage(msg)
138+
}
128139
// 发送消息
129140
const handleSendMessage = async () => {
130141
// 1. 校验消息是否为空
131142
if (isEmpty(unref(message.value))) {
132143
messageTool.warning('请输入消息后再发送哦!')
144+
return
133145
}
134146
// 2. 组织发送消息
135147
const msg = {
136148
conversationId: keFuConversation.value.id,
137149
contentType: KeFuMessageContentTypeEnum.TEXT,
138150
content: message.value
139151
}
152+
await sendMessage(msg)
153+
}
154+
155+
// 发送消息 【共用】
156+
const sendMessage = async (msg: any) => {
140157
await KeFuMessageApi.sendKeFuMessage(msg)
141158
message.value = ''
142159
// 3. 加载消息列表
@@ -152,20 +169,17 @@ const scrollToBottom = async () => {
152169
await nextTick()
153170
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
154171
}
155-
156-
/** 图预览 */
157-
const imagePreview = (imgUrl: string) => {
158-
createImageViewer({
159-
urlList: [imgUrl]
160-
})
161-
}
162-
163-
// TODO puhui999: 轮训相关,功能完善后移除
164-
onBeforeUnmount(() => {
165-
if (!poller.value) {
166-
return
172+
/**
173+
* 是否显示时间
174+
* @param {*} item - 数据
175+
* @param {*} index - 索引
176+
*/
177+
const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
178+
if (unref(messageList.value)[index + 1]) {
179+
let dateString = dayjs(unref(messageList.value)[index + 1].createTime).fromNow()
180+
return dateString !== dayjs(unref(item).createTime).fromNow()
167181
}
168-
clearInterval(poller.value)
182+
return false
169183
})
170184
</script>
171185

@@ -241,14 +255,24 @@ onBeforeUnmount(() => {
241255
transform: scale(1.03);
242256
}
243257
}
258+
259+
.date-message,
260+
.system-message {
261+
width: fit-content;
262+
border-radius: 12rpx;
263+
padding: 8rpx 16rpx;
264+
margin-bottom: 16rpx;
265+
background-color: #e8e8e8;
266+
color: #999;
267+
font-size: 24rpx;
268+
}
244269
}
245270
246271
.chat-tools {
247272
width: 100%;
248273
border: #e4e0e0 solid 1px;
274+
border-radius: 10px;
249275
height: 44px;
250-
display: flex;
251-
align-items: center;
252276
}
253277
254278
::v-deep(textarea) {

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

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,33 +35,16 @@
3535

3636
<script lang="ts" setup>
3737
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
38-
import { replaceEmoji } from '@/views/mall/promotion/kefu/components/emoji'
39-
import { formatDate, getNowDateTime } from '@/utils/formatTime'
40-
import { KeFuMessageContentTypeEnum } from '@/views/mall/promotion/kefu/components/constants'
38+
import { useEmoji } from './tools/emoji'
39+
import { formatDate } from '@/utils/formatTime'
40+
import { KeFuMessageContentTypeEnum } from './tools/constants'
4141
4242
defineOptions({ name: 'KeFuConversationBox' })
43+
const { replaceEmoji } = useEmoji()
4344
const activeConversationIndex = ref(-1) // 选中的会话
4445
const conversationList = ref<KeFuConversationRespVO[]>([]) // 会话列表
4546
const getConversationList = async () => {
4647
conversationList.value = await KeFuConversationApi.getConversationList()
47-
// 测试数据
48-
for (let i = 0; i < 5; i++) {
49-
conversationList.value.push({
50-
id: 1,
51-
userId: 283,
52-
userAvatar:
53-
'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132',
54-
userNickname: '辉辉鸭' + i,
55-
lastMessageTime: getNowDateTime(),
56-
lastMessageContent:
57-
'[爱心][爱心]你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇',
58-
lastMessageContentType: 1,
59-
adminPinned: false,
60-
userDeleted: false,
61-
adminDeleted: false,
62-
adminUnreadMessageCount: 19
63-
})
64-
}
6548
}
6649
defineExpose({ getConversationList })
6750
const emits = defineEmits<{
@@ -72,22 +55,6 @@ const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
7255
activeConversationIndex.value = index
7356
emits('change', item)
7457
}
75-
const poller = ref<any>(null) // TODO puhui999: 轮训定时器,暂时模拟 websocket
76-
onMounted(() => {
77-
// TODO puhui999: 轮训相关,功能完善后移除
78-
if (!poller.value) {
79-
poller.value = setInterval(() => {
80-
getConversationList()
81-
}, 1000)
82-
}
83-
})
84-
// TODO puhui999: 轮训相关,功能完善后移除
85-
onBeforeUnmount(() => {
86-
if (!poller.value) {
87-
return
88-
}
89-
clearInterval(poller.value)
90-
})
9158
</script>
9259

9360
<style lang="scss" scoped>
4.14 KB
2.25 KB
4.33 KB
3.7 KB
3.68 KB
4.34 KB
3.89 KB

0 commit comments

Comments
 (0)