!470 【新增】:mall 客服选择并发送图片信息

Merge pull request !470 from puhui999/dev-crm
pull/471/head^2
芋道源码 2024-07-04 13:55:32 +00:00 committed by Gitee
commit cabfc5ccc0
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
60 changed files with 410 additions and 180 deletions

View File

@ -29,9 +29,5 @@ VITE_BASE_PATH=/
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://localhost:3000'
# TODO puhui999这个可以不走 cdn 地址么?
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
VITE_STATIC_URL = https://file.sheepjs.com
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false

View File

@ -6,75 +6,69 @@
<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 p-10px">
<!-- TODO puhui999: 消息相关等后续完成后统一抽离封装 -->
<!-- 文本消息 -->
<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`
: ''
]"
class="flex items-center"
></div>
</template>
<!-- 图片消息 -->
<template v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType">
<div
:class="[
item.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: item.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
class="flex items-center"
>
<el-image
:src="item.content"
fit="contain"
style="width: 200px; height: 200px"
@click="imagePreview(item.content)"
/>
</div>
</template>
<div v-for="(item, index) in messageList" :key="item.id" class="w-[100%]">
<div class="flex justify-center items-center mb-20px">
<!-- 日期 -->
<div
v-if="
item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)
"
class="date-message"
>
{{ formatDate(item.createTime) }}
</div>
<!-- 系统消息 -->
<view
v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
class="system-message"
>
{{ item.content }}
</view>
</div>
<div
:class="[
item.senderType === UserTypeEnum.MEMBER
? `ss-row-left`
: item.senderType === UserTypeEnum.ADMIN
? `ss-row-right`
: ''
]"
class="flex mb-20px w-[100%]"
>
<el-avatar
v-if="item.senderType === UserTypeEnum.MEMBER"
:src="keFuConversation.userAvatar"
alt="avatar"
/>
<div
:class="{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
class="p-10px"
>
<!-- 文本消息 -->
<TextMessageItem :message="item" />
<!-- 图片消息 -->
<ImageMessageItem :message="item" />
</div>
<el-avatar
v-if="item.senderType === UserTypeEnum.ADMIN"
:src="item.senderAvatar"
alt="avatar"
/>
</div>
<el-avatar
v-show="item.senderType === UserTypeEnum.ADMIN"
:src="item.senderAvatar"
alt="avatar"
/>
</div>
</div>
</el-scrollbar>
</el-main>
<el-footer height="230px">
<div class="h-[100%]">
<div class="chat-tools">
<div class="chat-tools flex items-center">
<EmojiSelectPopover @select-emoji="handleEmojiSelect" />
<PictureSelectUpload
class="ml-15px mt-3px cursor-pointer"
@send-picture="handleSendPicture"
/>
</div>
<el-input v-model="message" :rows="6" type="textarea" />
<el-input v-model="message" :rows="6" style="border-style: none" type="textarea" />
<div class="h-45px flex justify-end">
<el-button class="mt-10px" type="primary" @click="handleSendMessage"></el-button>
</div>
@ -88,19 +82,25 @@
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 EmojiSelectPopover from './EmojiSelectPopover.vue'
import { Emoji, replaceEmoji } from './emoji'
import { KeFuMessageContentTypeEnum } from './constants'
import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
import PictureSelectUpload from './tools/PictureSelectUpload.vue'
import TextMessageItem from './message/TextMessageItem.vue'
import ImageMessageItem from './message/ImageMessageItem.vue'
import { Emoji } from './tools/emoji'
import { KeFuMessageContentTypeEnum } from './tools/constants'
import { isEmpty } from '@/utils/is'
import { UserTypeEnum } from '@/utils/constants'
import { createImageViewer } from '@/components/ImageViewer'
import { formatDate } from '@/utils/formatTime'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
defineOptions({ name: 'KeFuMessageBox' })
const messageTool = useMessage()
const message = ref('') //
const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const poller = ref<any>(null) // TODO puhui999: websocket
// TODO puhui999:
const getMessageList = async (conversation: KeFuConversationRespVO) => {
keFuConversation.value = conversation
@ -111,25 +111,37 @@ const getMessageList = async (conversation: KeFuConversationRespVO) => {
messageList.value = list.reverse()
// TODO puhui999:
await scrollToBottom()
// TODO puhui999:
if (!poller.value) {
poller.value = setInterval(() => {
getMessageList(conversation)
}, 1000)
}
}
defineExpose({ getMessageList })
//
const refreshMessageList = () => {
if (!keFuConversation.value) {
return
}
getMessageList(keFuConversation.value)
}
defineExpose({ getMessageList, refreshMessageList })
//
const showChatBox = computed(() => !isEmpty(keFuConversation.value))
//
const handleEmojiSelect = (item: Emoji) => {
message.value += item.name
}
//
const handleSendPicture = async (picUrl: string) => {
//
const msg = {
conversationId: keFuConversation.value.id,
contentType: KeFuMessageContentTypeEnum.IMAGE,
content: picUrl
}
await sendMessage(msg)
}
//
const handleSendMessage = async () => {
// 1.
if (isEmpty(unref(message.value))) {
messageTool.warning('请输入消息后再发送哦!')
return
}
// 2.
const msg = {
@ -137,6 +149,11 @@ const handleSendMessage = async () => {
contentType: KeFuMessageContentTypeEnum.TEXT,
content: message.value
}
await sendMessage(msg)
}
//
const sendMessage = async (msg: any) => {
await KeFuMessageApi.sendKeFuMessage(msg)
message.value = ''
// 3.
@ -152,20 +169,17 @@ const scrollToBottom = async () => {
await nextTick()
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
}
/** 图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
// TODO puhui999:
onBeforeUnmount(() => {
if (!poller.value) {
return
/**
* 是否显示时间
* @param {*} item - 数据
* @param {*} index - 索引
*/
const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
if (unref(messageList.value)[index + 1]) {
let dateString = dayjs(unref(messageList.value)[index + 1].createTime).fromNow()
return dateString !== dayjs(unref(item).createTime).fromNow()
}
clearInterval(poller.value)
return false
})
</script>
@ -241,14 +255,24 @@ onBeforeUnmount(() => {
transform: scale(1.03);
}
}
.date-message,
.system-message {
width: fit-content;
border-radius: 12rpx;
padding: 8rpx 16rpx;
margin-bottom: 16rpx;
background-color: #e8e8e8;
color: #999;
font-size: 24rpx;
}
}
.chat-tools {
width: 100%;
border: #e4e0e0 solid 1px;
border-radius: 10px;
height: 44px;
display: flex;
align-items: center;
}
::v-deep(textarea) {

View File

@ -35,33 +35,16 @@
<script lang="ts" setup>
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { replaceEmoji } from '@/views/mall/promotion/kefu/components/emoji'
import { formatDate, getNowDateTime } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from '@/views/mall/promotion/kefu/components/constants'
import { useEmoji } from './tools/emoji'
import { formatDate } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from './tools/constants'
defineOptions({ name: 'KeFuConversationBox' })
const { replaceEmoji } = useEmoji()
const activeConversationIndex = ref(-1) //
const conversationList = ref<KeFuConversationRespVO[]>([]) //
const getConversationList = async () => {
conversationList.value = await KeFuConversationApi.getConversationList()
//
for (let i = 0; i < 5; i++) {
conversationList.value.push({
id: 1,
userId: 283,
userAvatar:
'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132',
userNickname: '辉辉鸭' + i,
lastMessageTime: getNowDateTime(),
lastMessageContent:
'[爱心][爱心]你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇你好哇',
lastMessageContentType: 1,
adminPinned: false,
userDeleted: false,
adminDeleted: false,
adminUnreadMessageCount: 19
})
}
}
defineExpose({ getConversationList })
const emits = defineEmits<{
@ -72,22 +55,6 @@ const openRightMessage = (item: KeFuConversationRespVO, index: number) => {
activeConversationIndex.value = index
emits('change', item)
}
const poller = ref<any>(null) // TODO puhui999: websocket
onMounted(() => {
// TODO puhui999:
if (!poller.value) {
poller.value = setInterval(() => {
getConversationList()
}, 1000)
}
})
// TODO puhui999:
onBeforeUnmount(() => {
if (!poller.value) {
return
}
clearInterval(poller.value)
})
</script>
<style lang="scss" scoped>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1720063872285" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6895"
width="200" height="200">
<path d="M782.16 880.98c-179.31 23.91-361 23.91-540.32 0C138.89 867.25 62 779.43 62 675.57V348.43c0-103.86 76.89-191.69 179.84-205.41 179.31-23.91 361-23.91 540.31 0C885.11 156.75 962 244.57 962 348.43v327.13c0 103.87-76.89 191.69-179.84 205.42z"
fill="#FF554D" p-id="6896"></path>
<path d="M226.11 596.86c-9.74 47.83 17.26 95.6 63.48 111.3C333.49 723.08 394.55 737 469.53 737c59.25 0 105.46-8.69 140.23-19.7 51.59-16.34 79.94-71.16 63.37-122.68-24.47-76.11-65.57-180.7-106.68-180.7-64.62 0-64.62 96.92-64.62 96.92S437.22 317 372.61 317c-82.11 0-117.85 139.12-146.5 279.86z"
fill="#FFFFFF" p-id="6897"></path>
<path d="M782 347m-60 0a60 60 0 1 0 120 0 60 60 0 1 0-120 0Z" fill="#FFBC55" p-id="6898"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,5 +1,5 @@
import KeFuConversationBox from './KeFuConversationBox.vue'
import KeFuChatBox from './KeFuChatBox.vue'
import * as Constants from './constants'
import * as Constants from './tools/constants'
export { KeFuConversationBox, KeFuChatBox, Constants }

View File

@ -0,0 +1,39 @@
<template>
<!-- 图片消息 -->
<template v-if="KeFuMessageContentTypeEnum.IMAGE === message.contentType">
<div
:class="[
message.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: message.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
>
<el-image
:src="message.content"
fit="contain"
style="width: 200px"
@click="imagePreview(message.content)"
/>
</div>
</template>
</template>
<script lang="ts" setup>
import { KeFuMessageContentTypeEnum } from '../tools/constants'
import { UserTypeEnum } from '@/utils/constants'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { createImageViewer } from '@/components/ImageViewer'
defineOptions({ name: 'ImageMessageItem' })
defineProps<{
message: KeFuMessageRespVO
}>()
/** 图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
</script>

View File

@ -0,0 +1,29 @@
<template>
<!-- 文本消息 -->
<template v-if="KeFuMessageContentTypeEnum.TEXT === message.contentType">
<div
v-dompurify-html="replaceEmoji(message.content)"
:class="[
message.senderType === UserTypeEnum.MEMBER
? `ml-10px`
: message.senderType === UserTypeEnum.ADMIN
? `mr-10px`
: ''
]"
class="flex items-center"
></div>
</template>
</template>
<script lang="ts" setup>
import { KeFuMessageContentTypeEnum } from '../tools/constants'
import { UserTypeEnum } from '@/utils/constants'
import { useEmoji } from '../tools/emoji'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
defineOptions({ name: 'TextMessageItem' })
defineProps<{
message: KeFuMessageRespVO
}>()
const { replaceEmoji } = useEmoji()
</script>

View File

@ -2,7 +2,7 @@
<template>
<el-popover :width="500" placement="top" trigger="click">
<template #reference>
<Icon :size="30" class="ml-10px" icon="twemoji:grinning-face" />
<Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" />
</template>
<ElScrollbar height="300px">
<ul class="ml-2 flex flex-wrap px-2">
@ -26,8 +26,9 @@
<script lang="ts" setup>
defineOptions({ name: 'EmojiSelectPopover' })
import { Emoji, getEmojiList } from './emoji'
import { Emoji, useEmoji } from './emoji'
const { getEmojiList } = useEmoji()
const emojiList = computed(() => getEmojiList())
const emits = defineEmits<{

View File

@ -0,0 +1,91 @@
<template>
<div>
<img :src="Picture" style="width: 35px; height: 35px" @click="selectAndUpload" />
</div>
</template>
<script lang="ts" setup>
import Picture from '@/views/mall/promotion/kefu/components/images/picture.svg'
import * as FileApi from '@/api/infra/file'
defineOptions({ name: 'PictureSelectUpload' })
const message = useMessage()
const emits = defineEmits<{
(e: 'send-picture', v: string): void
}>()
//
const selectAndUpload = async () => {
const files: any = await getFiles()
message.success('图片发送中请稍等。。。')
const res = await FileApi.updateFile({ file: files[0].file })
emits('send-picture', res.data)
}
/**
* 唤起文件选择窗口并获取选择的文件
* @param {Object} options - 配置选项
* @param {boolean} [options.multiple=true] - 是否支持多选
* @param {string} [options.accept=''] - 文件上传格式限制
* @param {number} [options.limit=1] - 单次上传最大文件数
* @param {number} [options.fileSize=500] - 单个文件大小限制单位MB
* @returns {Promise<Array>} 选择的文件列表每个文件带有一个uid
*/
async function getFiles(options = {}) {
const { multiple, accept, limit, fileSize } = {
multiple: true,
accept: 'image/jpeg, image/png, image/gif', //
limit: 1,
fileSize: 500,
...options
}
//
const input = document.createElement('input')
input.type = 'file'
input.style.display = 'none'
if (multiple) input.multiple = true
if (accept) input.accept = accept
//
document.body.appendChild(input)
//
input.click()
// change
try {
const files = await new Promise((resolve, reject) => {
input.addEventListener('change', (event: any) => {
const filesArray = Array.from(event?.target?.files || [])
//
document.body.removeChild(input)
//
if (filesArray.length > limit) {
reject({ errorType: 'limit', files: filesArray })
return
}
//
const oversizedFiles = filesArray.filter((file: File) => file.size / 1024 ** 2 > fileSize)
if (oversizedFiles.length > 0) {
reject({ errorType: 'fileSize', files: oversizedFiles })
return
}
// uid
const fileList = filesArray.map((file, index) => ({ file, uid: Date.now() + index }))
resolve(fileList)
})
})
return files
} catch (error) {
console.error('选择文件出错:', error)
throw error
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -1,3 +1,4 @@
// 客服消息类型枚举类
export const KeFuMessageContentTypeEnum = {
TEXT: 1, // 文本消息
IMAGE: 2, // 图片消息
@ -8,3 +9,8 @@ export const KeFuMessageContentTypeEnum = {
PRODUCT: 10, // 商品消息
ORDER: 11 // 订单消息"
}
// Promotion 的 WebSocket 消息类型枚举类
export const WebSocketMessageTypeConstants = {
KEFU_MESSAGE_TYPE: 'kefu_message_type', // 客服消息类型
KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change' // 客服消息管理员已读
}

View File

@ -1,4 +1,6 @@
export const emojiList = [
import { isEmpty } from '@/utils/is'
const emojiList = [
{ name: '[笑掉牙]', file: 'xiaodiaoya.png' },
{ name: '[可爱]', file: 'keai.png' },
{ name: '[冷酷]', file: 'lengku.png' },
@ -54,53 +56,60 @@ export interface Emoji {
url: string
}
export const emojiPage = {}
emojiList.forEach((item, index) => {
if (!emojiPage[Math.floor(index / 30) + 1]) {
emojiPage[Math.floor(index / 30) + 1] = []
}
emojiPage[Math.floor(index / 30) + 1].push(item)
})
// 后端上传地址
const staticUrl = import.meta.env.VITE_STATIC_URL
// 后缀
const suffix = '/static/img/chat/emoji/'
// 处理表情
export function replaceEmoji(data: string) {
let newData = data
if (typeof newData !== 'object') {
const reg = /\[(.+?)\]/g // [] 中括号
const zhEmojiName = newData.match(reg)
if (zhEmojiName) {
zhEmojiName.forEach((item) => {
const emojiFile = selEmojiFile(item)
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${
staticUrl + suffix + emojiFile
}"/>`
)
})
export const useEmoji = () => {
const emojiPathList = ref<any[]>([])
// 加载本地图片
const getStaticEmojiPath = async () => {
const pathList = import.meta.glob(
'@/views/mall/promotion/kefu/components/images/*.{png,jpg,jpeg,svg}'
)
for (const path in pathList) {
const imageModule: any = await pathList[path]()
emojiPathList.value.push(imageModule.default)
}
}
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) {
return emojiList[index].file
// 初始化
onMounted(async () => {
if (isEmpty(emojiPathList.value)) {
await getStaticEmojiPath()
}
})
// 处理表情
function replaceEmoji(data: string) {
let newData = data
if (typeof newData !== 'object') {
const reg = /\[(.+?)\]/g // [] 中括号
const zhEmojiName = newData.match(reg)
if (zhEmojiName) {
zhEmojiName.forEach((item) => {
const emojiFile = selEmojiFile(item)
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
)
})
}
}
return newData
}
return false
// 获得所有表情
function getEmojiList(): Emoji[] {
return emojiList.map((item) => ({
url: selEmojiFile(item.name),
name: item.name
})) as Emoji[]
}
function selEmojiFile(name: string) {
for (const emoji of emojiList) {
if (emoji.name === name) {
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
}
}
return false
}
return { replaceEmoji, getEmojiList }
}

View File

@ -16,19 +16,77 @@
<script lang="ts" setup>
import { KeFuChatBox, KeFuConversationBox } from './components'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getAccessToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
import { WebSocketMessageTypeConstants } from '@/views/mall/promotion/kefu/components/tools/constants'
defineOptions({ name: 'KeFu' })
const message = useMessage()
//
const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getMessageList(conversation)
}
//
//======================= websocket start=======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws/').replace('http', 'ws') +
'?token=' +
getAccessToken()
) // WebSocket
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
autoReconnect: false,
heartbeat: true
})
watchEffect(() => {
if (!data.value) {
return
}
try {
// 1.
if (data.value === 'pong') {
// state.recordList.push({
// text: '',
// time: new Date().getTime()
// })
return
}
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
if (!type) {
message.error('未知的消息类型:' + data.value)
return
}
// 2.2 KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
//
getConversationList()
//
keFuChatBoxRef.value?.refreshMessageList()
return
}
} catch (error) {
console.error(error)
}
})
//======================= websocket end=======================
//
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationBox>>()
onMounted(() => {
const getConversationList = () => {
keFuConversationRef.value?.getConversationList()
}
onMounted(() => {
getConversationList()
// websocket
open()
})
onBeforeUnmount(() => {
// websocket
close()
})
</script>