Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
9c05ff35
authored
Jul 02, 2024
by
puhui999
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【新增】:mall 客服实现 emoji 表情选择和消息发送
parent
1abfec76
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
151 additions
and
189 deletions
+151
-189
src/api/mall/promotion/kefu/message/index.ts
+1
-1
src/views/mall/promotion/kefu/components/EmojiSelectPopover.vue
+40
-0
src/views/mall/promotion/kefu/components/KeFuChatBox.vue
+94
-187
src/views/mall/promotion/kefu/components/emoji.ts
+16
-1
No files found.
src/api/mall/promotion/kefu/message/index.ts
View file @
9c05ff35
...
...
@@ -51,7 +51,7 @@ export interface KeFuMessageRespVO {
export
const
KeFuMessageApi
=
{
// 发送客服消息
sendKeFuMessage
:
async
(
data
:
any
)
=>
{
return
await
request
.
p
u
t
({
return
await
request
.
p
os
t
({
url
:
'/promotion/kefu-message/send'
,
data
})
...
...
src/views/mall/promotion/kefu/components/EmojiSelectPopover.vue
0 → 100644
View file @
9c05ff35
<!-- emoji 表情选择组件 -->
<
template
>
<el-popover
:width=
"500"
placement=
"top"
trigger=
"click"
>
<template
#
reference
>
<Icon
:size=
"30"
class=
"ml-10px"
icon=
"twemoji:grinning-face"
/>
</
template
>
<ElScrollbar
height=
"300px"
>
<ul
class=
"ml-2 flex flex-wrap px-2"
>
<li
v-for=
"(item, index) in emojiList"
:key=
"index"
:style=
"{
borderColor: 'var(--el-color-primary)',
color: 'var(--el-color-primary)'
}"
:title=
"item.name"
class=
"icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
@
click=
"handleSelect(item)"
>
<img
:src=
"item.url"
style=
"width: 24px; height: 24px"
/>
</li>
</ul>
</ElScrollbar>
</el-popover>
</template>
<
script
lang=
"ts"
setup
>
defineOptions
({
name
:
'EmojiSelectPopover'
})
import
{
Emoji
,
getEmojiList
}
from
'./emoji'
const
emojiList
=
computed
(()
=>
getEmojiList
())
const
emits
=
defineEmits
<
{
(
e
:
'select-emoji'
,
v
:
Emoji
)
}
>
()
const
handleSelect
=
(
item
:
Emoji
)
=>
{
// 整个 emoji 数据传递出去,方便以后输入框直接显示表情
emits
(
'select-emoji'
,
item
)
}
</
script
>
src/views/mall/promotion/kefu/components/KeFuChatBox.vue
View file @
9c05ff35
...
...
@@ -3,58 +3,61 @@
<el-header>
<div
class=
"kefu-title"
>
{{
keFuConversation
.
userNickname
}}
</div>
</el-header>
<el-main
class=
"kefu-content"
>
<div
v-for=
"item in messageList"
:key=
"item.id"
:class=
"[
item.senderType === UserTypeEnum.MEMBER
? `ss-row-left`
: item.senderType === UserTypeEnum.ADMIN
? `ss-row-right`
: ''
]"
class=
"flex mb-20px w-[100%]"
>
<el-avatar
v-show=
"item.senderType === UserTypeEnum.MEMBER"
:src=
"keFuConversation.userAvatar"
alt=
"avatar"
/>
<div
class=
"kefu-message flex items-center p-10px"
>
<!-- 文本消息 -->
<template
v-if=
"KeFuMessageContentTypeEnum.TEXT === item.contentType"
>
<div
v-dompurify-html=
"replaceEmoji(item.content)"
:class=
"[
item.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: item.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
></div>
</
template
>
<
template
v-else
>
{{
item
.
content
}}
</
template
>
<el-main
class=
"kefu-content"
style=
"overflow: visible"
>
<el-scrollbar
ref=
"scrollbarRef"
always
height=
"calc(100vh - 495px)"
>
<div
ref=
"innerRef"
class=
"w-[100%] pb-3px"
>
<div
v-for=
"item in messageList"
:key=
"item.id"
:class=
"[
item.senderType === UserTypeEnum.MEMBER
? `ss-row-left`
: item.senderType === UserTypeEnum.ADMIN
? `ss-row-right`
: ''
]"
class=
"flex mb-20px w-[100%]"
>
<el-avatar
v-show=
"item.senderType === UserTypeEnum.MEMBER"
:src=
"keFuConversation.userAvatar"
alt=
"avatar"
/>
<div
class=
"kefu-message flex items-center p-10px"
>
<!-- 文本消息 -->
<template
v-if=
"KeFuMessageContentTypeEnum.TEXT === item.contentType"
>
<div
v-dompurify-html=
"replaceEmoji(item.content)"
:class=
"[
item.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: item.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
></div>
</
template
>
<
template
v-else
>
{{
item
.
content
}}
</
template
>
</div>
<el-avatar
v-show=
"item.senderType === UserTypeEnum.ADMIN"
:src=
"item.senderAvatar"
alt=
"avatar"
/>
</div>
</div>
<el-avatar
v-show=
"item.senderType === UserTypeEnum.ADMIN"
:src=
"item.senderAvatar"
alt=
"avatar"
/>
</div>
</el-scrollbar>
</el-main>
<!-- TODO puhui999: 发送下次提交完善 -->
<el-footer
height=
"230px"
>
<div
class=
"h-[100%]"
>
<div
class=
"chat-tools"
>
<
Icon
:size=
"30"
class=
"ml-10px"
icon=
"fa:frown-o
"
/>
<
EmojiSelectPopover
@
select-emoji=
"handleEmojiSelect
"
/>
</div>
<el-input
v-model=
"message"
:rows=
"6"
type=
"textarea"
/>
<div
class=
"h-45px flex justify-end"
>
<el-button
class=
"mt-10px"
type=
"primary"
>
发送
</el-button>
<el-button
class=
"mt-10px"
type=
"primary"
@
click=
"handleSendMessage"
>
发送
</el-button>
</div>
</div>
</el-footer>
...
...
@@ -63,162 +66,66 @@
</template>
<
script
lang=
"ts"
setup
>
import
{
KeFuMessageRespVO
}
from
'@/api/mall/promotion/kefu/message'
import
{
ElScrollbar
as
ElScrollbarType
}
from
'element-plus'
import
{
KeFuMessageApi
,
KeFuMessageRespVO
}
from
'@/api/mall/promotion/kefu/message'
import
{
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
UserTypeEnum
}
from
'@/utils/constants
'
import
{
replaceEmoji
}
from
'@/views/mall/promotion/kefu/components
/emoji'
import
{
KeFuMessageContentTypeEnum
}
from
'
@/views/mall/promotion/kefu/components
/constants'
import
EmojiSelectPopover
from
'./EmojiSelectPopover.vue
'
import
{
Emoji
,
replaceEmoji
}
from
'.
/emoji'
import
{
KeFuMessageContentTypeEnum
}
from
'
.
/constants'
import
{
isEmpty
}
from
'@/utils/is'
import
{
UserTypeEnum
}
from
'@/utils/constants'
defineOptions
({
name
:
'KeFuMessageBox'
})
const
messageTool
=
useMessage
()
const
message
=
ref
(
''
)
// 消息
const
messageList
=
ref
<
KeFuMessageRespVO
[]
>
([])
// 消息列表
const
keFuConversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
// 获得消息
// 获得消息 TODO puhui999: 先不考虑下拉加载历史消息
const
getMessageList
=
async
(
conversation
:
KeFuConversationRespVO
)
=>
{
keFuConversation
.
value
=
conversation
// const { list } = await KeFuMessageApi.getKeFuMessagePage({
// pageNo: 1,
// conversationId: conversation.id
// })
// TODO puhui999: 方便查看效果
const
list
=
[
{
id
:
19
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
2
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[爱心][爱心][坏笑][坏笑][天使][天使]'
,
readStatus
:
false
,
createTime
:
1718616705000
},
{
id
:
18
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[瞌睡][瞌睡]'
,
readStatus
:
false
,
createTime
:
1718616690000
},
{
id
:
17
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[冷酷][冷酷]'
,
readStatus
:
false
,
createTime
:
1718616350000
},
{
id
:
16
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[天使]'
,
readStatus
:
false
,
createTime
:
1718615505000
},
{
id
:
15
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
2
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[天使]'
,
readStatus
:
false
,
createTime
:
1718615485000
},
{
id
:
14
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
2
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[心碎][心碎]'
,
readStatus
:
false
,
createTime
:
1718615453000
},
{
id
:
13
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
2
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[心碎][心碎]'
,
readStatus
:
false
,
createTime
:
1718615430000
},
{
id
:
12
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[心碎][心碎]'
,
readStatus
:
false
,
createTime
:
1718615425000
},
{
id
:
11
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[困~][困~]'
,
readStatus
:
false
,
createTime
:
1718615413000
},
{
id
:
10
,
conversationId
:
1
,
senderId
:
283
,
senderAvatar
:
null
,
senderType
:
1
,
receiverId
:
null
,
receiverType
:
null
,
contentType
:
1
,
content
:
'[睡着][睡着]'
,
readStatus
:
false
,
createTime
:
1718615407000
}
]
messageList
.
value
=
list
const
{
list
}
=
await
KeFuMessageApi
.
getKeFuMessagePage
({
pageNo
:
1
,
conversationId
:
conversation
.
id
})
messageList
.
value
=
list
.
reverse
()
// TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
await
scrollToBottom
()
}
defineExpose
({
getMessageList
})
// 是否显示聊天区域
const
showChatBox
=
computed
(()
=>
!
isEmpty
(
keFuConversation
.
value
))
// 处理表情选择
const
handleEmojiSelect
=
(
item
:
Emoji
)
=>
{
message
.
value
+=
item
.
name
}
// 发送消息
const
handleSendMessage
=
async
()
=>
{
// 1. 校验消息是否为空
if
(
isEmpty
(
unref
(
message
.
value
)))
{
messageTool
.
warning
(
'请输入消息后再发送哦!'
)
}
// 2. 组织发送消息
const
msg
=
{
conversationId
:
keFuConversation
.
value
.
id
,
contentType
:
KeFuMessageContentTypeEnum
.
TEXT
,
content
:
message
.
value
}
await
KeFuMessageApi
.
sendKeFuMessage
(
msg
)
message
.
value
=
''
// 3. 加载消息列表
await
getMessageList
(
keFuConversation
.
value
)
// 滚动到最新消息处
await
scrollToBottom
()
}
const
innerRef
=
ref
<
HTMLDivElement
>
()
const
scrollbarRef
=
ref
<
InstanceType
<
typeof
ElScrollbarType
>>
()
// 滚动到底部
const
scrollToBottom
=
async
()
=>
{
await
nextTick
()
scrollbarRef
.
value
!
.
setScrollTop
(
innerRef
.
value
!
.
clientHeight
)
}
</
script
>
<
style
lang=
"scss"
scoped
>
...
...
src/views/mall/promotion/kefu/components/emoji.ts
View file @
9c05ff35
...
...
@@ -49,6 +49,11 @@ export const emojiList = [
{
name
:
'[恶魔]'
,
file
:
'emo.png'
}
]
export
interface
Emoji
{
name
:
string
url
:
string
}
export
const
emojiPage
=
{}
emojiList
.
forEach
((
item
,
index
)
=>
{
if
(
!
emojiPage
[
Math
.
floor
(
index
/
30
)
+
1
])
{
...
...
@@ -59,6 +64,8 @@ emojiList.forEach((item, index) => {
// 后端上传地址
const
staticUrl
=
import
.
meta
.
env
.
VITE_STATIC_URL
// 后缀
const
suffix
=
'/static/img/chat/emoji/'
// 处理表情
export
function
replaceEmoji
(
data
:
string
)
{
...
...
@@ -72,7 +79,7 @@ export function replaceEmoji(data: string) {
newData
=
newData
.
replace
(
item
,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="
${
staticUrl
+
'/static/img/chat/emoji/'
+
emojiFile
staticUrl
+
suffix
+
emojiFile
}
"/>`
)
})
...
...
@@ -81,6 +88,14 @@ export function replaceEmoji(data: string) {
return
newData
}
// 获得所有表情
export
function
getEmojiList
():
Emoji
[]
{
return
emojiList
.
map
((
item
)
=>
({
url
:
staticUrl
+
suffix
+
item
.
file
,
name
:
item
.
name
}))
as
Emoji
[]
}
function
selEmojiFile
(
name
:
string
)
{
for
(
const
index
in
emojiList
)
{
if
(
emojiList
[
index
].
name
===
name
)
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment