!478 完善 mall 客服

Merge pull request !478 from puhui999/dev-crm
pull/469/head
芋道源码 2024-07-11 00:41:35 +00:00 committed by Gitee
commit 392603c811
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
60 changed files with 155 additions and 178 deletions

View File

@ -1,16 +1,15 @@
<template> <template>
<div class="kefu"> <div class="kefu">
<div <div
v-for="(item, index) in conversationList" v-for="item in conversationList"
:key="item.id" :key="item.id"
:class="{ active: index === activeConversationIndex, pinned: item.adminPinned }" :class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
class="kefu-conversation flex items-center" class="kefu-conversation flex items-center"
@click="openRightMessage(item, index)" @click="openRightMessage(item)"
@contextmenu.prevent="rightClick($event as PointerEvent, item)" @contextmenu.prevent="rightClick($event as PointerEvent, item)"
> >
<div class="flex justify-center items-center w-100%"> <div class="flex justify-center items-center w-100%">
<!-- TODO style 换成 unocss --> <div class="flex justify-center items-center w-50px h-50px">
<div class="flex justify-center items-center" style="width: 50px; height: 50px">
<!-- 头像 + 未读 --> <!-- 头像 + 未读 -->
<el-badge <el-badge
:hidden="item.adminUnreadMessageCount === 0" :hidden="item.adminUnreadMessageCount === 0"
@ -27,19 +26,13 @@
{{ formatDate(item.lastMessageTime) }} {{ formatDate(item.lastMessageTime) }}
</span> </span>
</div> </div>
<!-- 文本消息 --> <!-- 最后聊天内容 -->
<template v-if="KeFuMessageContentTypeEnum.TEXT === item.lastMessageContentType"> <div
<div v-dompurify-html="
v-dompurify-html="replaceEmoji(item.lastMessageContent)" getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
class="last-message flex items-center color-[#989EA6]" "
></div> class="last-message flex items-center color-[#989EA6]"
</template> ></div>
<!-- 图片消息 -->
<template v-else>
<div class="last-message flex items-center color-[#989EA6]">
{{ getContentType(item.lastMessageContentType) }}
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -47,7 +40,7 @@
<!-- 右键进行操作类似微信 --> <!-- 右键进行操作类似微信 -->
<ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul"> <ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
<li <li
v-show="!selectedConversation.adminPinned" v-show="!rightClickConversation.adminPinned"
class="flex items-center" class="flex items-center"
@click.stop="updateConversationPinned(true)" @click.stop="updateConversationPinned(true)"
> >
@ -55,7 +48,7 @@
置顶会话 置顶会话
</li> </li>
<li <li
v-show="selectedConversation.adminPinned" v-show="rightClickConversation.adminPinned"
class="flex items-center" class="flex items-center"
@click.stop="updateConversationPinned(false)" @click.stop="updateConversationPinned(false)"
> >
@ -79,18 +72,22 @@ import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotio
import { useEmoji } from './tools/emoji' import { useEmoji } from './tools/emoji'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import { KeFuMessageContentTypeEnum } from './tools/constants' import { KeFuMessageContentTypeEnum } from './tools/constants'
import { useAppStore } from '@/store/modules/app'
defineOptions({ name: 'KeFuConversationBox' }) defineOptions({ name: 'KeFuConversationList' })
const message = useMessage() // const message = useMessage() //
const appStore = useAppStore()
const { replaceEmoji } = useEmoji() const { replaceEmoji } = useEmoji()
const conversationList = ref<KeFuConversationRespVO[]>([]) // const conversationList = ref<KeFuConversationRespVO[]>([]) //
const activeConversationIndex = ref(-1) // index TODO @puhui999 activeConversationId const activeConversationId = ref(-1) //
const collapse = computed(() => appStore.getCollapse) //
/** 加载会话列表 */ /** 加载会话列表 */
const getConversationList = async () => { const getConversationList = async () => {
conversationList.value = await KeFuConversationApi.getConversationList() const list = await KeFuConversationApi.getConversationList()
list.sort((a: KeFuConversationRespVO, _) => (a.adminPinned ? -1 : 1))
conversationList.value = list
} }
defineExpose({ getConversationList }) defineExpose({ getConversationList })
@ -98,45 +95,48 @@ defineExpose({ getConversationList })
const emits = defineEmits<{ const emits = defineEmits<{
(e: 'change', v: KeFuConversationRespVO): void (e: 'change', v: KeFuConversationRespVO): void
}>() }>()
const openRightMessage = (item: KeFuConversationRespVO, index: number) => { const openRightMessage = (item: KeFuConversationRespVO) => {
activeConversationIndex.value = index activeConversationId.value = item.id
emits('change', item) emits('change', item)
} }
// TODO @puhui999 getConversationDisplayText replaceEmoji
/** 获得消息类型 */ /** 获得消息类型 */
const getContentType = computed(() => (lastMessageContentType: number) => { const getConversationDisplayText = computed(
switch (lastMessageContentType) { () => (lastMessageContentType: number, lastMessageContent: string) => {
case KeFuMessageContentTypeEnum.SYSTEM: switch (lastMessageContentType) {
return '[系统消息]' case KeFuMessageContentTypeEnum.SYSTEM:
case KeFuMessageContentTypeEnum.VIDEO: return '[系统消息]'
return '[视频消息]' case KeFuMessageContentTypeEnum.VIDEO:
case KeFuMessageContentTypeEnum.IMAGE: return '[视频消息]'
return '[图片消息]' case KeFuMessageContentTypeEnum.IMAGE:
case KeFuMessageContentTypeEnum.PRODUCT: return '[图片消息]'
return '[商品消息]' case KeFuMessageContentTypeEnum.PRODUCT:
case KeFuMessageContentTypeEnum.ORDER: return '[商品消息]'
return '[订单消息]' case KeFuMessageContentTypeEnum.ORDER:
case KeFuMessageContentTypeEnum.VOICE: return '[订单消息]'
return '[语音消息]' case KeFuMessageContentTypeEnum.VOICE:
default: return '[语音消息]'
return '' case KeFuMessageContentTypeEnum.TEXT:
return replaceEmoji(lastMessageContent)
default:
return ''
}
} }
}) )
//======================= ======================= //======================= =======================
const showRightMenu = ref(false) // const showRightMenu = ref(false) //
const rightMenuStyle = ref<any>({}) // Style const rightMenuStyle = ref<any>({}) // Style
const selectedConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // TODO puhui999 rightClickConversation selected const rightClickConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
/** 打开右键菜单 */ /** 打开右键菜单 */
const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => { const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
selectedConversation.value = item rightClickConversation.value = item
// //
showRightMenu.value = true showRightMenu.value = true
rightMenuStyle.value = { rightMenuStyle.value = {
top: mouseEvent.clientY - 110 + 'px', top: mouseEvent.clientY - 110 + 'px',
left: mouseEvent.clientX - 80 + 'px' left: collapse.value ? mouseEvent.clientX - 80 + 'px' : mouseEvent.clientX - 210 + 'px'
} }
} }
/** 关闭右键菜单 */ /** 关闭右键菜单 */
@ -148,7 +148,7 @@ const closeRightMenu = () => {
const updateConversationPinned = async (adminPinned: boolean) => { const updateConversationPinned = async (adminPinned: boolean) => {
// 1. / // 1. /
await KeFuConversationApi.updateConversationPinned({ await KeFuConversationApi.updateConversationPinned({
id: selectedConversation.value.id, id: rightClickConversation.value.id,
adminPinned adminPinned
}) })
message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功') message.notifySuccess(adminPinned ? '置顶成功' : '取消置顶成功')
@ -161,7 +161,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
const deleteConversation = async () => { const deleteConversation = async () => {
// 1. // 1.
await message.confirm('您确定要删除该会话吗?') await message.confirm('您确定要删除该会话吗?')
await KeFuConversationApi.deleteConversation(selectedConversation.value.id) await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
// 2. // 2.
closeRightMenu() closeRightMenu()
await getConversationList() await getConversationList()

View File

@ -1,21 +1,11 @@
<template> <template>
<el-container v-if="showChatBox" class="kefu"> <el-container v-if="showKeFuMessageList" class="kefu">
<el-header> <el-header>
<!-- TODO @puhui999keFuConversation => conversation --> <div class="kefu-title">{{ conversation.userNickname }}</div>
<div class="kefu-title">{{ keFuConversation.userNickname }}</div>
</el-header> </el-header>
<!-- TODO @puhui999unocss --> <el-main class="kefu-content overflow-visible">
<el-main class="kefu-content" style="overflow: visible">
<!-- 加载历史消息 -->
<div
v-show="loadingMore"
class="loadingMore flex justify-center items-center cursor-pointer"
@click="handleOldMessage"
>
加载更多
</div>
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll"> <el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)" @scroll="handleScroll">
<div ref="innerRef" class="w-[100%] pb-3px"> <div v-if="refreshContent" ref="innerRef" class="w-[100%] pb-3px">
<!-- 消息列表 --> <!-- 消息列表 -->
<div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]"> <div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
<div class="flex justify-center items-center mb-20px"> <div class="flex justify-center items-center mb-20px">
@ -48,7 +38,7 @@
> >
<el-avatar <el-avatar
v-if="item.senderType === UserTypeEnum.MEMBER" v-if="item.senderType === UserTypeEnum.MEMBER"
:src="keFuConversation.userAvatar" :src="conversation.userAvatar"
alt="avatar" alt="avatar"
/> />
<div <div
@ -121,36 +111,48 @@ import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
defineOptions({ name: 'KeFuMessageBox' }) defineOptions({ name: 'KeFuMessageList' })
const message = ref('') // const message = ref('') //
const messageTool = useMessage() const messageTool = useMessage()
const messageList = ref<KeFuMessageRespVO[]>([]) // const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const showNewMessageTip = ref(false) // const showNewMessageTip = ref(false) //
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10,
conversationId: 0 conversationId: 0
}) })
const total = ref(0) // const total = ref(0) //
const refreshContent = ref(false) // ,
/** 获得消息列表 */ /** 获得消息列表 */
const getMessageList = async (conversation: KeFuConversationRespVO) => { const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
keFuConversation.value = conversation // ,
queryParams.conversationId = conversation.id if (conversationChange) {
const messageTotal = messageList.value.length queryParams.pageNo = 1
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) { messageList.value = []
return total.value = 0
loadHistory.value = false
refreshContent.value = false
} }
conversation.value = val
queryParams.conversationId = val.id
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams) const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
total.value = res.total total.value = res.total
for (const item of res.list) { //
if (messageList.value.some((val) => val.id === item.id)) { if (queryParams.pageNo === 1) {
continue messageList.value = res.list
} else {
//
for (const item of res.list) {
if (messageList.value.some((val) => val.id === item.id)) {
continue
}
messageList.value.push(item)
} }
messageList.value.push(item)
} }
refreshContent.value = true
await scrollToBottom() await scrollToBottom()
} }
@ -162,20 +164,24 @@ const getMessageList0 = computed(() => {
/** 刷新消息列表 */ /** 刷新消息列表 */
const refreshMessageList = async () => { const refreshMessageList = async () => {
if (!keFuConversation.value) { if (!conversation.value) {
return return
} }
queryParams.pageNo = 1 queryParams.pageNo = 1
await getMessageList(keFuConversation.value) await getMessageList(conversation.value, false)
if (loadHistory.value) { if (loadHistory.value) {
// //
showNewMessageTip.value = true showNewMessageTip.value = true
} }
} }
defineExpose({ getMessageList, refreshMessageList }) defineExpose({ getMessageList, refreshMessageList })
const showChatBox = computed(() => !isEmpty(keFuConversation.value)) // const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) //
const skipGetMessageList = computed(() => {
//
return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
}) //
/** 处理表情选择 */ /** 处理表情选择 */
const handleEmojiSelect = (item: Emoji) => { const handleEmojiSelect = (item: Emoji) => {
@ -186,7 +192,7 @@ const handleEmojiSelect = (item: Emoji) => {
const handleSendPicture = async (picUrl: string) => { const handleSendPicture = async (picUrl: string) => {
// //
const msg = { const msg = {
conversationId: keFuConversation.value.id, conversationId: conversation.value.id,
contentType: KeFuMessageContentTypeEnum.IMAGE, contentType: KeFuMessageContentTypeEnum.IMAGE,
content: picUrl content: picUrl
} }
@ -202,7 +208,7 @@ const handleSendMessage = async () => {
} }
// 2. // 2.
const msg = { const msg = {
conversationId: keFuConversation.value.id, conversationId: conversation.value.id,
contentType: KeFuMessageContentTypeEnum.TEXT, contentType: KeFuMessageContentTypeEnum.TEXT,
content: message.value content: message.value
} }
@ -215,7 +221,7 @@ const sendMessage = async (msg: any) => {
await KeFuMessageApi.sendKeFuMessage(msg) await KeFuMessageApi.sendKeFuMessage(msg)
message.value = '' message.value = ''
// //
await getMessageList(keFuConversation.value) await getMessageList(conversation.value, false)
// //
await scrollToBottom() await scrollToBottom()
} }
@ -233,7 +239,7 @@ const scrollToBottom = async () => {
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight) scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
showNewMessageTip.value = false showNewMessageTip.value = false
// 2.2 // 2.2
await KeFuMessageApi.updateKeFuMessageReadStatus(keFuConversation.value.id) await KeFuMessageApi.updateKeFuMessageReadStatus(conversation.value.id)
} }
/** 查看新消息 */ /** 查看新消息 */
@ -243,23 +249,28 @@ const handleToNewMessage = async () => {
} }
/** 加载历史消息 */ /** 加载历史消息 */
const loadingMore = ref(false) //
const loadHistory = ref(false) // const loadHistory = ref(false) //
const handleScroll = async ({ scrollTop }) => { const handleScroll = async ({ scrollTop }) => {
const messageTotal = messageList.value.length if (skipGetMessageList.value) {
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
return return
} }
// 20 //
loadingMore.value = scrollTop < 20 if (scrollTop === 0) {
await handleOldMessage()
}
} }
const handleOldMessage = async () => { const handleOldMessage = async () => {
//
const oldPageHeight = innerRef.value?.clientHeight
if (!oldPageHeight) {
return
}
loadHistory.value = true loadHistory.value = true
// //
queryParams.pageNo += 1 queryParams.pageNo += 1
await getMessageList(keFuConversation.value) await getMessageList(conversation.value, false)
loadingMore.value = false //
// TODO puhui999: scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
} }
/** /**
@ -288,20 +299,6 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
&-content { &-content {
position: relative; position: relative;
.loadingMore {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #eee;
color: #666;
text-align: center;
line-height: 50px;
transform: translateY(-100%);
transition: transform 0.3s ease-in-out;
}
.newMessageTip { .newMessageTip {
position: absolute; position: absolute;
bottom: 35px; bottom: 35px;

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,4 +1,4 @@
import KeFuConversationBox from './KeFuConversationBox.vue' import KeFuConversationList from './KeFuConversationList.vue'
import KeFuChatBox from './KeFuChatBox.vue' import KeFuMessageList from './KeFuMessageList.vue'
export { KeFuConversationBox, KeFuChatBox } export { KeFuConversationList, KeFuMessageList }

View File

@ -10,12 +10,13 @@
: '' : ''
]" ]"
> >
<!-- TODO @puhui999unocss -->
<el-image <el-image
:initial-index="0"
:preview-src-list="[message.content]"
:src="message.content" :src="message.content"
class="w-200px"
fit="contain" fit="contain"
style="width: 200px" preview-teleported
@click="imagePreview(message.content)"
/> />
</div> </div>
</template> </template>
@ -25,17 +26,9 @@
import { KeFuMessageContentTypeEnum } from '../tools/constants' import { KeFuMessageContentTypeEnum } from '../tools/constants'
import { UserTypeEnum } from '@/utils/constants' import { UserTypeEnum } from '@/utils/constants'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message' import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { createImageViewer } from '@/components/ImageViewer'
defineOptions({ name: 'ImageMessageItem' }) defineOptions({ name: 'ImageMessageItem' })
defineProps<{ defineProps<{
message: KeFuMessageRespVO message: KeFuMessageRespVO
}>() }>()
/** 图预览 */
const imagePreview = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
</script> </script>

View File

@ -18,10 +18,9 @@
</div> </div>
</div> </div>
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom"> <div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
<!-- TODO @puhui999要不把 img => picUrl 类似这种搞的更匹配一点 -->
<ProductItem <ProductItem
:img="item.picUrl"
:num="item.count" :num="item.count"
:picUrl="item.picUrl"
:price="item.price" :price="item.price"
:skuText="item.properties.map((property: any) => property.valueName).join(' ')" :skuText="item.properties.map((property: any) => property.valueName).join(' ')"
:title="item.spuName" :title="item.spuName"
@ -61,7 +60,7 @@ const getMessageContent = computed(() => JSON.parse(props.message.content))
* @param order 订单 * @param order 订单
* @return {string} 颜色的 class 名称 * @return {string} 颜色的 class 名称
*/ */
function formatOrderColor(order) { function formatOrderColor(order: any) {
if (order.status === 0) { if (order.status === 0) {
return 'info-color' return 'info-color'
} }
@ -79,7 +78,7 @@ function formatOrderColor(order) {
* *
* @param order 订单 * @param order 订单
*/ */
function formatOrderStatus(order) { function formatOrderStatus(order: any) {
if (order.status === 0) { if (order.status === 0) {
return '待付款' return '待付款'
} }
@ -109,23 +108,23 @@ function formatOrderStatus(order) {
background-color: #e2e2e2; background-color: #e2e2e2;
.order-card-header { .order-card-header {
height: 80rpx; height: 80px;
.order-no { .order-no {
font-size: 26rpx; font-size: 26px;
font-weight: 500; font-weight: 500;
} }
} }
.pay-box { .pay-box {
.discounts-title { .discounts-title {
font-size: 24rpx; font-size: 24px;
line-height: normal; line-height: normal;
color: #999999; color: #999999;
} }
.discounts-money { .discounts-money {
font-size: 24rpx; font-size: 24px;
line-height: normal; line-height: normal;
color: #999; color: #999;
font-family: OPPOSANS; font-family: OPPOSANS;
@ -137,29 +136,29 @@ function formatOrderStatus(order) {
} }
.order-card-footer { .order-card-footer {
height: 100rpx; height: 100px;
.more-item-box { .more-item-box {
padding: 20rpx; padding: 20px;
.more-item { .more-item {
height: 60rpx; height: 60px;
.title { .title {
font-size: 26rpx; font-size: 26px;
} }
} }
} }
.more-btn { .more-btn {
color: #999999; color: #999999;
font-size: 24rpx; font-size: 24px;
} }
.content { .content {
width: 154rpx; width: 154px;
color: #333333; color: #333333;
font-size: 26rpx; font-size: 26px;
font-weight: 500; font-weight: 500;
} }
} }

View File

@ -8,7 +8,14 @@
class="ss-order-card-warp flex items-stretch justify-between bg-white" class="ss-order-card-warp flex items-stretch justify-between bg-white"
> >
<div class="img-box mr-24px"> <div class="img-box mr-24px">
<el-image :src="img" class="order-img" fit="contain" @click="imagePrediv(img)" /> <el-image
:initial-index="0"
:preview-src-list="[picUrl]"
:src="picUrl"
class="order-img"
fit="contain"
preview-teleported
/>
</div> </div>
<div <div
:style="[{ width: titleWidth ? titleWidth + 'px' : '' }]" :style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
@ -44,12 +51,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { createImageViewer } from '@/components/ImageViewer'
import { fenToYuan } from '@/utils' import { fenToYuan } from '@/utils'
defineOptions({ name: 'ProductItem' }) defineOptions({ name: 'ProductItem' })
const props = defineProps({ const props = defineProps({
img: { picUrl: {
type: String, type: String,
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto' default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
}, },
@ -101,14 +107,6 @@ const skuString = computed(() => {
} }
return props.skuText return props.skuText
}) })
// TODO @puhui999使 preview-teleported
/** 图预览 */
const imagePrediv = (imgUrl: string) => {
createImageViewer({
urlList: [imgUrl]
})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,7 +11,7 @@
]" ]"
> >
<ProductItem <ProductItem
:img="getMessageContent.picUrl" :picUrl="getMessageContent.picUrl"
:price="getMessageContent.price" :price="getMessageContent.price"
:skuText="getMessageContent.introduction" :skuText="getMessageContent.introduction"
:title="getMessageContent.spuName" :title="getMessageContent.spuName"

View File

@ -17,8 +17,7 @@
class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2" 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)" @click="handleSelect(item)"
> >
<!-- TODO @puhui999换成 unocss --> <img :src="item.url" class="w-24px h-24px" />
<img :src="item.url" style="width: 24px; height: 24px" />
</li> </li>
</ul> </ul>
</ElScrollbar> </ElScrollbar>

View File

@ -1,14 +1,12 @@
<!-- 图片选择 --> <!-- 图片选择 -->
<template> <template>
<div> <div>
<!-- TODO @puhui999unocss --> <img :src="Picture" class="w-35px h-35px" @click="selectAndUpload" />
<img :src="Picture" style="width: 35px; height: 35px" @click="selectAndUpload" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// TODO @puhui999images asserts import Picture from '@/views/mall/promotion/kefu/components/asserts/picture.svg'
import Picture from '@/views/mall/promotion/kefu/components/images/picture.svg'
import * as FileApi from '@/api/infra/file' import * as FileApi from '@/api/infra/file'
defineOptions({ name: 'PictureSelectUpload' }) defineOptions({ name: 'PictureSelectUpload' })

View File

@ -59,12 +59,10 @@ export interface Emoji {
export const useEmoji = () => { export const useEmoji = () => {
const emojiPathList = ref<any[]>([]) const emojiPathList = ref<any[]>([])
// TODO @puhui999initStaticEmoji 会不会更好
/** 加载本地图片 */ /** 加载本地图片 */
const getStaticEmojiPath = async () => { const initStaticEmoji = async () => {
// TODO @puhui999images 改成 asserts 更合适哈。
const pathList = import.meta.glob( const pathList = import.meta.glob(
'@/views/mall/promotion/kefu/components/images/*.{png,jpg,jpeg,svg}' '@/views/mall/promotion/kefu/components/asserts/*.{png,jpg,jpeg,svg}'
) )
for (const path in pathList) { for (const path in pathList) {
const imageModule: any = await pathList[path]() const imageModule: any = await pathList[path]()
@ -75,26 +73,24 @@ export const useEmoji = () => {
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
if (isEmpty(emojiPathList.value)) { if (isEmpty(emojiPathList.value)) {
await getStaticEmojiPath() await initStaticEmoji()
} }
}) })
// TODO @puhui999建议 function 都改成 const 这种来定义哈。保持统一风格
/** /**
* *
* *
* @param data TODO @puhui999data => content * @param data
* @return * @return
*/ */
function replaceEmoji(data: string) { const replaceEmoji = (content: string) => {
let newData = data let newData = content
if (typeof newData !== 'object') { if (typeof newData !== 'object') {
// TODO @puhui999 \] 是不是可以简化成 ]。我看 idea 提示了哈 const reg = /\[(.+?)]/g // [] 中括号
const reg = /\[(.+?)\]/g // [] 中括号
const zhEmojiName = newData.match(reg) const zhEmojiName = newData.match(reg)
if (zhEmojiName) { if (zhEmojiName) {
zhEmojiName.forEach((item) => { zhEmojiName.forEach((item) => {
const emojiFile = selEmojiFile(item) const emojiFile = getEmojiFileByName(item)
newData = newData.replace( newData = newData.replace(
item, item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>` `<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
@ -112,13 +108,12 @@ export const useEmoji = () => {
*/ */
function getEmojiList(): Emoji[] { function getEmojiList(): Emoji[] {
return emojiList.map((item) => ({ return emojiList.map((item) => ({
url: selEmojiFile(item.name), url: getEmojiFileByName(item.name),
name: item.name name: item.name
})) as Emoji[] })) as Emoji[]
} }
// TODO @puhui999getEmojiFileByName 会不会更容易理解哈 function getEmojiFileByName(name: string) {
function selEmojiFile(name: string) {
for (const emoji of emojiList) { for (const emoji of emojiList) {
if (emoji.name === name) { if (emoji.name === name) {
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1) return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)

View File

@ -1,23 +1,22 @@
<template> <template>
<el-row :gutter="10"> <el-row :gutter="10">
<!-- TODO @puhui999KeFuConversationBox => KeFuConversationList KeFuChatBox => KeFuMessageList -->
<!-- 会话列表 --> <!-- 会话列表 -->
<el-col :span="8"> <el-col :span="8">
<ContentWrap> <ContentWrap>
<KeFuConversationBox ref="keFuConversationRef" @change="handleChange" /> <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
</ContentWrap> </ContentWrap>
</el-col> </el-col>
<!-- 会话详情选中会话的消息列表 --> <!-- 会话详情选中会话的消息列表 -->
<el-col :span="16"> <el-col :span="16">
<ContentWrap> <ContentWrap>
<KeFuChatBox ref="keFuChatBoxRef" @change="getConversationList" /> <KeFuMessageList ref="keFuChatBoxRef" @change="getConversationList" />
</ContentWrap> </ContentWrap>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { KeFuChatBox, KeFuConversationBox } from './components' import { KeFuConversationList, KeFuMessageList } from './components'
import { WebSocketMessageTypeConstants } from './components/tools/constants' import { WebSocketMessageTypeConstants } from './components/tools/constants'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation' import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getAccessToken } from '@/utils/auth' import { getAccessToken } from '@/utils/auth'
@ -36,7 +35,7 @@ const server = ref(
/** 发起 WebSocket 连接 */ /** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, { const { data, close, open } = useWebSocket(server.value, {
autoReconnect: false, // TODO @puhui999 autoReconnect: true,
heartbeat: true heartbeat: true
}) })
@ -76,17 +75,16 @@ watchEffect(() => {
} }
}) })
// ======================= WebSocket end ======================= // ======================= WebSocket end =======================
/** 加载会话列表 */ /** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationBox>>() const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
const getConversationList = () => { const getConversationList = () => {
keFuConversationRef.value?.getConversationList() keFuConversationRef.value?.getConversationList()
} }
/** 加载指定会话的消息列表 */ /** 加载指定会话的消息列表 */
const keFuChatBoxRef = ref<InstanceType<typeof KeFuChatBox>>() const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
const handleChange = (conversation: KeFuConversationRespVO) => { const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getMessageList(conversation) keFuChatBoxRef.value?.getMessageList(conversation, true)
} }
/** 初始化 */ /** 初始化 */