diff --git a/.env.local b/.env.local index 82a3f72a..005d2f0d 100644 --- a/.env.local +++ b/.env.local @@ -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 diff --git a/src/views/mall/promotion/kefu/components/KeFuChatBox.vue b/src/views/mall/promotion/kefu/components/KeFuChatBox.vue index bda7c483..c755cecf 100644 --- a/src/views/mall/promotion/kefu/components/KeFuChatBox.vue +++ b/src/views/mall/promotion/kefu/components/KeFuChatBox.vue @@ -6,75 +6,69 @@
-
- -
- - - - - +
+
+ +
+ {{ formatDate(item.createTime) }} +
+ + + {{ item.content }} + +
+
+ +
+ + + + +
+
-
-
+
+
- +
发送
@@ -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([]) // 消息列表 const keFuConversation = ref({} as KeFuConversationRespVO) // 用户会话 -const poller = ref(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 }) @@ -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) { diff --git a/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue b/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue index 19984db2..4a0e036f 100644 --- a/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue +++ b/src/views/mall/promotion/kefu/components/KeFuConversationBox.vue @@ -35,33 +35,16 @@ diff --git a/src/views/mall/promotion/kefu/components/constants.ts b/src/views/mall/promotion/kefu/components/tools/constants.ts similarity index 50% rename from src/views/mall/promotion/kefu/components/constants.ts rename to src/views/mall/promotion/kefu/components/tools/constants.ts index f8599160..e171e363 100644 --- a/src/views/mall/promotion/kefu/components/constants.ts +++ b/src/views/mall/promotion/kefu/components/tools/constants.ts @@ -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' // 客服消息管理员已读 +} diff --git a/src/views/mall/promotion/kefu/components/emoji.ts b/src/views/mall/promotion/kefu/components/tools/emoji.ts similarity index 58% rename from src/views/mall/promotion/kefu/components/emoji.ts rename to src/views/mall/promotion/kefu/components/tools/emoji.ts index 765d2a6e..449f51c0 100644 --- a/src/views/mall/promotion/kefu/components/emoji.ts +++ b/src/views/mall/promotion/kefu/components/tools/emoji.ts @@ -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, - `` - ) - }) +export const useEmoji = () => { + const emojiPathList = ref([]) + // 加载本地图片 + 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, + `` + ) + }) + } + } + 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 } } diff --git a/src/views/mall/promotion/kefu/index.vue b/src/views/mall/promotion/kefu/index.vue index 1204b8dc..1cfea211 100644 --- a/src/views/mall/promotion/kefu/index.vue +++ b/src/views/mall/promotion/kefu/index.vue @@ -16,19 +16,77 @@