|
@ -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()
|
|
@ -1,21 +1,11 @@
|
|||
<template>
|
||||
<el-container v-if="showChatBox" class="kefu">
|
||||
<el-container v-if="showKeFuMessageList" class="kefu">
|
||||
<el-header>
|
||||
<!-- TODO @puhui999:keFuConversation => conversation -->
|
||||
<div class="kefu-title">{{ keFuConversation.userNickname }}</div>
|
||||
<div class="kefu-title">{{ conversation.userNickname }}</div>
|
||||
</el-header>
|
||||
<!-- TODO @puhui999:unocss -->
|
||||
<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;
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
@ -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 }
|
||||
|
|
|
@ -10,12 +10,13 @@
|
|||
: ''
|
||||
]"
|
||||
>
|
||||
<!-- TODO @puhui999:unocss -->
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
]"
|
||||
>
|
||||
<ProductItem
|
||||
:img="getMessageContent.picUrl"
|
||||
:picUrl="getMessageContent.picUrl"
|
||||
:price="getMessageContent.price"
|
||||
:skuText="getMessageContent.introduction"
|
||||
:title="getMessageContent.spuName"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
<!-- 图片选择 -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- TODO @puhui999:unocss -->
|
||||
<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 @puhui999:images 换成 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' })
|
||||
|
|
|
@ -59,12 +59,10 @@ export interface Emoji {
|
|||
export const useEmoji = () => {
|
||||
const emojiPathList = ref<any[]>([])
|
||||
|
||||
// TODO @puhui999:initStaticEmoji 会不会更好
|
||||
/** 加载本地图片 */
|
||||
const getStaticEmojiPath = async () => {
|
||||
// TODO @puhui999:images 改成 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 @puhui999:data => 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 @puhui999:getEmojiFileByName 会不会更容易理解哈
|
||||
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)
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
<template>
|
||||
<el-row :gutter="10">
|
||||
<!-- TODO @puhui999:KeFuConversationBox => 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)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
|
|