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

View File

@ -1,21 +1,11 @@
<template>
<el-container v-if="showChatBox" class="kefu">
<el-container v-if="showKeFuMessageList" class="kefu">
<el-header>
<!-- TODO @puhui999keFuConversation => conversation -->
<div class="kefu-title">{{ keFuConversation.userNickname }}</div>
<div class="kefu-title">{{ conversation.userNickname }}</div>
</el-header>
<!-- TODO @puhui999unocss -->
<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-main class="kefu-content overflow-visible">
<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 class="flex justify-center items-center mb-20px">
@ -48,7 +38,7 @@
>
<el-avatar
v-if="item.senderType === UserTypeEnum.MEMBER"
:src="keFuConversation.userAvatar"
:src="conversation.userAvatar"
alt="avatar"
/>
<div
@ -121,36 +111,48 @@ import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
defineOptions({ name: 'KeFuMessageBox' })
defineOptions({ name: 'KeFuMessageList' })
const message = ref('') //
const messageTool = useMessage()
const messageList = ref<KeFuMessageRespVO[]>([]) //
const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) //
const showNewMessageTip = ref(false) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
conversationId: 0
})
const total = ref(0) //
const refreshContent = ref(false) // ,
/** 获得消息列表 */
const getMessageList = async (conversation: KeFuConversationRespVO) => {
keFuConversation.value = conversation
queryParams.conversationId = conversation.id
const messageTotal = messageList.value.length
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
return
const getMessageList = async (val: KeFuConversationRespVO, conversationChange: boolean) => {
// ,
if (conversationChange) {
queryParams.pageNo = 1
messageList.value = []
total.value = 0
loadHistory.value = false
refreshContent.value = false
}
conversation.value = val
queryParams.conversationId = val.id
const res = await KeFuMessageApi.getKeFuMessagePage(queryParams)
total.value = res.total
for (const item of res.list) {
if (messageList.value.some((val) => val.id === item.id)) {
continue
//
if (queryParams.pageNo === 1) {
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()
}
@ -162,20 +164,24 @@ const getMessageList0 = computed(() => {
/** 刷新消息列表 */
const refreshMessageList = async () => {
if (!keFuConversation.value) {
if (!conversation.value) {
return
}
queryParams.pageNo = 1
await getMessageList(keFuConversation.value)
await getMessageList(conversation.value, false)
if (loadHistory.value) {
//
//
showNewMessageTip.value = true
}
}
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) => {
@ -186,7 +192,7 @@ const handleEmojiSelect = (item: Emoji) => {
const handleSendPicture = async (picUrl: string) => {
//
const msg = {
conversationId: keFuConversation.value.id,
conversationId: conversation.value.id,
contentType: KeFuMessageContentTypeEnum.IMAGE,
content: picUrl
}
@ -202,7 +208,7 @@ const handleSendMessage = async () => {
}
// 2.
const msg = {
conversationId: keFuConversation.value.id,
conversationId: conversation.value.id,
contentType: KeFuMessageContentTypeEnum.TEXT,
content: message.value
}
@ -215,7 +221,7 @@ const sendMessage = async (msg: any) => {
await KeFuMessageApi.sendKeFuMessage(msg)
message.value = ''
//
await getMessageList(keFuConversation.value)
await getMessageList(conversation.value, false)
//
await scrollToBottom()
}
@ -233,7 +239,7 @@ const scrollToBottom = async () => {
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
showNewMessageTip.value = false
// 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 handleScroll = async ({ scrollTop }) => {
const messageTotal = messageList.value.length
if (total.value > 0 && messageTotal > 0 && messageTotal === total.value) {
if (skipGetMessageList.value) {
return
}
// 20
loadingMore.value = scrollTop < 20
//
if (scrollTop === 0) {
await handleOldMessage()
}
}
const handleOldMessage = async () => {
//
const oldPageHeight = innerRef.value?.clientHeight
if (!oldPageHeight) {
return
}
loadHistory.value = true
//
queryParams.pageNo += 1
await getMessageList(keFuConversation.value)
loadingMore.value = false
// TODO puhui999:
await getMessageList(conversation.value, false)
//
scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
}
/**
@ -288,20 +299,6 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
&-content {
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 {
position: absolute;
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 KeFuChatBox from './KeFuChatBox.vue'
import KeFuConversationList from './KeFuConversationList.vue'
import KeFuMessageList from './KeFuMessageList.vue'
export { KeFuConversationBox, KeFuChatBox }
export { KeFuConversationList, KeFuMessageList }

View File

@ -10,12 +10,13 @@
: ''
]"
>
<!-- TODO @puhui999unocss -->
<el-image
:initial-index="0"
:preview-src-list="[message.content]"
:src="message.content"
class="w-200px"
fit="contain"
style="width: 200px"
@click="imagePreview(message.content)"
preview-teleported
/>
</div>
</template>
@ -25,17 +26,9 @@
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

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

View File

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

View File

@ -11,7 +11,7 @@
]"
>
<ProductItem
:img="getMessageContent.picUrl"
:picUrl="getMessageContent.picUrl"
:price="getMessageContent.price"
:skuText="getMessageContent.introduction"
: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"
@click="handleSelect(item)"
>
<!-- TODO @puhui999换成 unocss -->
<img :src="item.url" style="width: 24px; height: 24px" />
<img :src="item.url" class="w-24px h-24px" />
</li>
</ul>
</ElScrollbar>

View File

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

View File

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

View File

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